home *** CD-ROM | disk | FTP | other *** search
/ HPAVC / HPAVC CD-ROM.iso / WINER.ZIP / CHAP11.TXT < prev    next >
Text File  |  1994-09-04  |  147KB  |  3,347 lines

  1.                                 CHAPTER 11
  2.  
  3.                       ACCESSING DOS AND BIOS SERVICES
  4.  
  5.  
  6. BASIC is arguably the most capable of all the popular high-level languages
  7. available for the PC.  However, one area where all PC languages are weak is
  8. when accessing DOS and BIOS system interrupts.  Previous chapters included
  9. subroutines and functions that access DOS interrupt services using CALL
  10. Interrupt, but in most cases with little explanation.  This chapter
  11. explains what interrupts are, how they are accessed, and how they return
  12. information to your program.
  13.      Only assembly language--the native language of the processor in every
  14. PC--can directly access interrupts.  Assembly language programmers use the
  15. Int instruction, which transfers control to an *interrupt service routine*. 
  16. An Int instruction is nearly identical to a conventional CALL statement,
  17. except a slightly different mechanism within the computer's hardware is
  18. used to implement it.
  19.      BASIC lets you access system interrupts by providing a pair of
  20. assembly language interface routines called Interrupt and InterruptX. 
  21. These routines accept the interrupt number and other parameters the
  22. interrupt requires, and they then perform the actual interrupt call. 
  23. InterruptX is similar to Interrupt; the only real difference is that it
  24. lets you access two additional CPU registers.
  25.  
  26.  
  27. WHAT IS AN INTERRUPT?
  28. =====================
  29.  
  30. The IBM PC family of personal computers supports two types of interrupts:
  31. hardware and software.  A hardware interrupt is invoked by an external
  32. device or event, such as pressing a key on the keyboard.  When this
  33. happens, a signal is sent from the keyboard hardware to the PC's
  34. microprocessor telling it to stop what it's currently doing and instead
  35. call one of the routines in the PC's BIOS.
  36.      For example, while your PC is currently copying a group of files you
  37. may type DIR simultaneously, to display the results when the copying has
  38. finished.  Even though DOS is reading and writing the files, you interrupt
  39. those operations for a few microseconds each time a key is pressed.  The
  40. BIOS routine that handles the keyboard interrupt is responsible for placing
  41. the keystrokes into the PC's 15-character keyboard buffer.  Then when DOS
  42. has finished copying your files, the DIR command will already be there. 
  43. Because there is a direct physical connection between the keyboard
  44. circuitry and the PC's microprocessor, you are able to interrupt whatever
  45. else is happening at the time.
  46.      A software interrupt, on the other hand, doesn't really interrupt
  47. anything.  Rather, it is a form of CALL command that an assembly language
  48. program may issue.  Just like the CALL command in BASIC that transfers
  49. control to a subroutine, a software interrupt is used in an assembly
  50. language program to access DOS and BIOS services.  Although assembly
  51. language programs may use a CALL statement to invoke a subroutine, an
  52. interrupt instruction is needed to access the operating system routines.
  53.      When a program issues a subroutine call, the address of that
  54. subroutine must be known, so the processor will be able to jump to the code
  55. there.  With most programs, subroutine addresses are determined and
  56. assigned by LINK.EXE when it combines the various portions of your program
  57. into a single executable file.  But this method can't be used with the DOS
  58. and BIOS routines, because their addresses are not known ahead of time. 
  59. For example, if you compile a BASIC program on an IBM PC, it must also be
  60. able to be run on, say, a Tandy 1000 using a different version of DOS.  Of
  61. course, it is impossible for LINK to know where the DOS and BIOS routines
  62. are located on the Tandy computer.
  63.      To solve this problem and allow a program to call a routine whose
  64. address is not known, a list of addresses is stored in a known place in low
  65. memory.  This place is called the *interrupt vector table*.  The first
  66. 1,024 bytes in every PC contains a table of addresses for all 256 possible
  67. interrupts.  Each table entry requires two words (four bytes): one word is
  68. used to hold the routine's segment, and the other holds its address within
  69. that segment.  Whenever an assembly language program issues an interrupt
  70. instruction, the PC's processor automatically fetches the segment and
  71. address from this table, and then calls that address.  Thus, any program
  72. may access any interrupt routine, without having to know where in memory
  73. the routine actually resides.  The first four bytes in the interrupt vector
  74. table hold the address for Interrupt 0, the next four show where Interrupt
  75. 1 is, and so forth.
  76.      DOS and BIOS services are specified by interrupt number, and most
  77. interrupt routines also expect a *service number*.  Nearly all of the DOS
  78. services you will find useful are accessed through Interrupt &H21, with the
  79. desired service number specified in the AH register.  In many cases,
  80. information is also returned in the CPU registers.  For instance, the DOS
  81. service that returns the current default disk drive is specified by placing
  82. the value &H19 in the AH register.  When the interrupt has finished, the
  83. current drive number is returned in the AL register.  Registers will be
  84. described in the section that follows.  As with the low memory addresses
  85. discussed in Chapter 10, the DOS and BIOS interrupt numbers use Hexadecimal
  86. numbering by convention.
  87.      There are also several BIOS interrupts you will find useful, and these
  88. include video interrupt &H10, printer interrupt &H17, Print Screen
  89. interrupt 5, and the two equipment interrupts &H11 and &H12.  There are
  90. other BIOS and DOS interrupts, but those are mostly useful when accessed
  91. from assembly language.  For example, there is little need to call keyboard
  92. interrupt &H16 to read a key, since INKEY$ already does this.  Likewise,
  93. you are unlikely to find disk interrupt &H13 very interesting, although it
  94. is used when performing copy protection and other low-level direct disk
  95. accesses.  But unless you know what you are doing, it is possible--even
  96. likely--to trash your hard disk in the process of experimenting with this
  97. disk interrupt.
  98.      I won't attempt to provide all of the information you need to access
  99. every possible DOS and BIOS service here.  Indeed, a complete discussion
  100. would fill several books.  Two excellent books that I recommend are "Peter
  101. Norton's Programmer's Guide to the IBM PC" (1988), and "Advanced MS-DOS",
  102. by Ray Duncan (1988).  Both of these books are published by Microsoft
  103. Press, and can be found in most book stores.  These books list every DOS
  104. and BIOS interrupt service, and show which registers are used to exchange
  105. information with each interrupt service.
  106.      Also, once you have read and understood the information in this
  107. chapter you should go back to some of the examples presented in earlier
  108. chapters.  In particular, Chapter 6 shows how to access DOS Interrupt &H21
  109. to read file names, and Chapter 7 includes routines that access Interrupt
  110. &H2F to see if a network is running on the host PC and if so which one.
  111.  
  112.  
  113. REGISTERS
  114. =========
  115.  
  116. Microprocessors in the Intel 8086 family contain a set of built-in integer
  117. variables called *registers*.  Each register can hold a single word (two
  118. bytes), which nicely corresponds to the size of a BASIC integer variable. 
  119. Because these registers are contained within the microprocessor itself,
  120. they can be accessed by the CPU very quickly--much faster than variables
  121. which are stored in memory.
  122.      The 8086 and 8088 microprocessors contain a total of fourteen
  123. registers.  [Newer CPUs contain more registers, but they are not accessible
  124. via CALL Interrupt nor are they useful to a BASIC program.]  Some of these
  125. registers are intended for a specific use, while others may be used as
  126. general purpose variables.  For example, the CS and DS registers contain
  127. the current code and data segments respectively, while the CX register is
  128. often used as a counter in an assembly language FOR/NEXT loop.  I'm not
  129. going to pursue a lengthy discussion of microprocessor theory here though,
  130. because it's not really necessary if you simply want to access a few system
  131. interrupts.  Rather, I will focus on how to set up and invoke the various
  132. interrupt services, and interpret the results they return.  Assembly
  133. language and CPU registers will be discussed more fully in Chapter 12.
  134.      Both Interrupt and InterruptX (Interrupt Extended) require a TYPE
  135. variable with components that mirror each of the processor's registers. 
  136. Figure 11-1 lists all of the 8086 registers that are accessible from BASIC,
  137. showing which are available with each of the interrupt routines.
  138.  
  139.  
  140. InterruptX     Interrupt
  141. ==========     =========
  142.   AX             AX
  143.   BX             BX
  144.   CX             CX
  145.   DX             DX
  146.   BP             BP
  147.   SI             SI
  148.   DI             DI
  149.   Flags          Flags
  150.   DS
  151.   ES
  152.  
  153. Figure 11-1: The registers accessible from BASIC through Interrupt and
  154. InterruptX.
  155.  
  156.  
  157. When you call the either Interrupt routine, the values in a TYPE variable
  158. are copied into the CPU's registers, the interrupt is performed, and then
  159. the results returned in each register are copied back into a TYPE variable
  160. again.  All of the CALL Interrupt examples Microsoft shows use two TYPE
  161. variables called InRegs and OutRegs.  However, you can also use the same
  162. TYPE variable to both send and receive the register values.  In fact, using
  163. a single TYPE variable will save a few bytes of DGROUP memory.  Therefore,
  164. the remaining examples that use CALL Interrupt use a single TYPE variable.
  165.      One important issue that needs to be addressed before we can proceed
  166. is how the CPU registers are accessed.  I stated earlier that there are
  167. fourteen such registers, and each is the same size as an integer variable:
  168. 2 bytes.  While this is certainly true, there is more to the story.  Four
  169. of the registers--AX, BX, CX, and DX--can also be treated as being two
  170. separate one-byte registers.
  171.      Each register half uses the designator "H" or "L" to mean High or Low. 
  172. For example, the high-byte portion of AX is called AH, and the low-byte
  173. portion of CX is CL.  When considered as a composite register, the two
  174. halves form a single integer word.  Figure 11-2 shows how the AX register
  175. is constructed, with each half contributing to the total combined value.
  176.  
  177.  
  178. │<────────────────────  AX  ─────────────────────>│
  179. ╔════════════════════════╤════════════════════════╗
  180. ║ 1  1  0  1  0  0  0  1 │ 1  1  0  0  1  1  0  1 ║
  181. ╚════════════════════════╧════════════════════════╝
  182. │<───────── AH ─────────>│<───────── AL ─────────>│
  183.  
  184. Figure 11-2: How a single word-sized register may also be treated as two
  185. byte-sized registers.
  186.  
  187.  
  188. In an assembly language program it is simple to access each register half
  189. separately.  However, BASIC does not offer a byte-sized variable type to
  190. use within the TYPE declaration.  Therefore, a slight amount of math is
  191. required to get at each half separately.  Although a fixed-length string
  192. with a length of one character could be used, the added overhead BASIC
  193. imposes to access a string as a number reduces the usefulness of that
  194. approach.
  195.      Using Hexadecimal notation and multiplication simplifies access to
  196. each register half when it is being assigned, and integer division and
  197. BASIC's AND operator lets you separate the two halves when reading them. 
  198. That is, you can assign the value &H12 to the upper byte in AH and the
  199. value &H34 to the lower byte in AL at one time, like this:
  200.  
  201.      Registers.AX = &H1234
  202.  
  203. In many cases it is necessary to assign only AH, which can be done like
  204. this:
  205.  
  206.      Registers.AX = &H0600
  207.  
  208. Here, the value 6 is placed into AH, and 0 is assigned to AL.  Since many
  209. of the DOS and BIOS services ignore what is in AL, assigning a value of
  210. zero is the simplest and most effective solution.  Again, using Hexadecimal
  211. notation lets you clearly define what is in each register half, because the
  212. first two digits represent the upper portion, and the second two represent
  213. the lower byte.
  214.      When both the upper and lower bytes are important, you can use
  215. multiplication to assign them.  By definition, any byte value in the high
  216. portion of a register is 256 times greater than it would be in the lower
  217. part.  Thus, to assign the variable Low% to AL and High% to AH is as simple
  218. as this:
  219.  
  220.      Registers.AX = Low% + (256 * High%)
  221.  
  222. In practice the parentheses are not really necessary because multiplication
  223. is always performed before addition.  But I included them here for clarity.
  224.      When an interrupt routine returns information in one of the
  225. combination registers, you may easily isolate the high and low portions as
  226. follows:
  227.  
  228.      Low% = Registers.DX AND 255
  229.      High% = Registers.DX \ 256
  230.  
  231. Some examples you may have seen use MOD to extract the lower byte, and that
  232. will also work:
  233.  
  234.      Low% = Registers.DX MOD 256
  235.  
  236. Although MOD and AND cause BASIC to generate the same amount of assembly
  237. language code (three bytes), I generally prefer using AND because that
  238. instruction is somewhat faster on the older 8088 processors.
  239.  
  240.  
  241. ACCESSING THE BIOS
  242. ==================
  243.  
  244. The simplest BIOS interrupt to call is the Print Screen interrupt,
  245. Interrupt 5.  No parameters are required by this interrupt, and no values
  246. are returned when it finishes.  But since the Interrupt routine expects the
  247. TYPE variable to be present and copies data to it, you must still dimension
  248. it in your program.
  249.      Because Interrupt and InterruptX are external subroutines as opposed
  250. to built-in commands, you will need to load the Quick Library containing
  251. these routines.  QuickBASIC comes with the file QB.QLB; BASIC PDS provides
  252. the same routines in a library named QBX.QLB.  [And in VB/DOS this file is
  253. called VBDOS.QLB.]  You must of course use whichever is appropriate for
  254. your version of BASIC.  To start QuickBASIC and load the Quick Library that
  255. contains these routines use the /L switch like this:
  256.  
  257.      qb /l
  258.  
  259. Normally, the name of a Quick Library must be given after the /L switch. 
  260. However, QB and QBX know that /L by itself means to load the default QB.QLB
  261. or QBX.QLB Quick Library.
  262.      The following complete program prints a simple pattern on the screen,
  263. and then sends it to the printer designated as LPT1: as if the PrtSc key
  264. had been pressed.
  265.  
  266.  
  267. DEFINT A-Z
  268. TYPE RegType
  269.   AX AS INTEGER
  270.   BX AS INTEGER
  271.   CX AS INTEGER
  272.   DX AS INTEGER
  273.   BP AS INTEGER
  274.   SI AS INTEGER
  275.   DI AS INTEGER
  276.   Flags AS INTEGER
  277. END TYPE
  278. DIM Registers AS RegType
  279.  
  280. CLS
  281. FOR X% = 1 TO 24
  282.   PRINT STRING$(80, X% + 64);
  283. NEXT
  284. CALL Interrupt(5, Registers, Registers)
  285.  
  286.  
  287. Although the Registers TYPE definition is shown here, the remaining
  288. examples in this chapter will instead specify the REGTYPE.BI include file
  289. that contains this code.  QuickBASIC includes a similar include file called
  290. QB.BI, and BASIC PDS uses the name QBX.BI for the same file.  [I created
  291. REGTYPE.BI so all of the programs in this book will run as is with any
  292. version of BASIC.  But the BASIC-supplied versions also include DECLARE
  293. statements for the Interrupt routines, where my REGTYPE.BI file does not. 
  294. Since all of these programs use the CALL keyword, a declaration is not
  295. strictly necessary.]
  296.  
  297.  
  298. THE BIOS VIDEO INTERRUPT
  299.  
  300. The next example shows how to call BIOS video interrupt &H10 to clear just
  301. a portion of the display screen.  It is designed as a combination
  302. demonstration and subprogram, so you can extract just the subprogram and
  303. add it to programs of your own.
  304.  
  305.  
  306. DEFINT A-Z
  307. DECLARE SUB ClearScreen (ULRow, ULCol, LRRow, LRCol, Colr)
  308.  
  309. '$INCLUDE: 'REGTYPE.BI'
  310. DIM SHARED Registers AS RegType
  311.  
  312. CLS
  313. FG = 7: BG = 1           'set the foreground and background colors
  314. COLOR FG, BG
  315.  
  316. FOR X% = 1 TO 24
  317.   PRINT STRING$(80, X% + 64);
  318. NEXT
  319.  
  320. Colr = FG + 16 * BG      'use the same colors for clearing
  321. CALL ClearScreen(5, 10, 20, 70, Colr)
  322.  
  323. SUB ClearScreen (ULRow, ULCol, LRRow, LRCol, Colr) STATIC
  324.   Registers.AX = &H600
  325.   Registers.BX = Colr * 256
  326.   Registers.CX = (ULCol - 1) + (256 * (ULRow - 1))
  327.   Registers.DX = (LRCol - 1) + (256 * (LRRow - 1))
  328.   CALL Interrupt(&H10, Registers, Registers)
  329. END SUB
  330.  
  331.  
  332. There are two important benefits to using the BIOS for a routine such as
  333. this.  One is of course the reduced amount of code that is needed, when
  334. compared to manually looping through memory using POKE to clear each
  335. character position.  The second is the BIOS is responsible for determining
  336. the type of monitor installed, to select the correct video segment.
  337.      The demonstration portion of the program first clears the screen, and
  338. then creates a simple test pattern using a color of white on blue.  Just
  339. before the call to ClearScreen, the correct Colr parameter is calculated
  340. based on the same foreground and background specified to BASIC.  Where
  341. BASIC accepts separate foreground and background values, the BIOS requires
  342. a single composite color byte.
  343.      The simplified formula used in this example will accommodate normal
  344. colors, but does not support adding 16 to the foreground to specify a
  345. flashing color.  This next formula shows how to derive a single color byte
  346. while also honoring flashing:
  347.  
  348.      Colr = (FG AND 16) * 8 + ((BG AND 7) * 16) + (FG AND 15)
  349.  
  350. ClearScreen is then called telling it to clear a rectangular portion of the
  351. screen that lies within the boundary specified by an upper-left corner at
  352. location 5, 10 to the lower-right corner at location 20, 70.  The color
  353. value calculated earlier is also passed, so the white on blue color will be
  354. maintained even after the screen is cleared.
  355.      Within ClearScreen, four of the CPU's registers are assigned to values
  356. needed by the BIOS video interrupt.  The first statement specifies service
  357. 6 in AH, which tells the BIOS to scroll the screen.  The number of rows to
  358. scroll is then placed into the AL register, which we've set to zero.  This
  359. particular BIOS service recognizes zero as a special flag, which tells it
  360. to clear the screen rather than scroll it.
  361.      Service 6 also expects the color to use for clearing in the BH
  362. register.  As I explained earlier, multiplying by 256 is equivalent to
  363. assigning just the higher portion of an integer, so the statement
  364. Registers.BX = Colr * 256 is equivalent to placing the one byte that is
  365. actually used by the Colr variable into BH.
  366.      The next two instructions take the upper left and lower right corner
  367. arguments, and place them into the appropriate registers.  In this case,
  368. the upper left column is placed into CL and the upper left row in CH. 
  369. Similarly, the lower right column goes into DL and the lower right row into
  370. DH.  Even though BASIC considers screen rows and columns to be numbered
  371. beginning at 1, the BIOS routines assume these to be zero-based. 
  372. Therefore, 1 is subtracted from the parameters before they are placed into
  373. each component of the Registers TYPE variable.  Finally, BASIC's Interrupt
  374. routine is called specifying Interrupt number &H10.
  375.      Note that the same BIOS interrupt service can also be used to scroll a
  376. rectangular portion of the screen.  Indeed, this is the primary purpose of
  377. service 6.  To scroll a portion of the screen up a certain number of lines,
  378. you will place the number of lines into AL:
  379.  
  380.      Registers.AX = NumLines + (6 * 256)
  381.  
  382. Scrolling the screen downward is also possible, using service 7 like this:
  383.  
  384.      Registers.AX = NumLines + (7 * 256)
  385.  
  386. Also note that the Registers TYPE variable was dimensioned to be shared. 
  387. This allows it to be accessed from all of the subprograms in a single
  388. program.  If Registers is dimensioned in many different subprograms and
  389. functions, then a new instance will be created, with each stealing 20 bytes
  390. of DGROUP memory.  Beware, however, that this memory savings has the
  391. potential drawback of introducing subtle bugs due to the same variable
  392. being used by different services.  Whatever register values remain after
  393. one use of CALL Interrupt will still be present the next time, unless new
  394. values are explicitly assigned.  [But that is rarely a problem, since you
  395. will generally assign all of the registers that a given interrupt needs
  396. just before calling that interrupt.]
  397.      Although this short example simply clears or scrolls a portion of the
  398. display screen, it provides a foundation for nearly anything else you may
  399. need to do using CALL Interrupt.  The DOS interrupt examples that follow
  400. will build on this foundation, and show how to access a wealth of useful
  401. services that are not otherwise possible using BASIC alone.
  402.  
  403.  
  404. ACCESSING DOS INTERRUPTS
  405. ========================
  406.  
  407. As with the BIOS video interrupt services, DOS interrupt &H21 expects a
  408. service number to be given in the AH register.  Many DOS services require
  409. additional information in other registers as well, including integer values
  410. and the segments and addresses of variables.
  411.      The DOS services that accept or return a string (such as a file or
  412. directory name) require the address of the string, to know where it is
  413. located.  For example, the DOS service that changes the current directory
  414. is called with AH set to &H3B, and DS:DX holding the address of a string
  415. that contains the name of the directory to change to.
  416.      Likewise, to obtain the current directory you would load AH with the
  417. value &H47, and DS:SI with the address of a string that will receive the
  418. current directory's name.  It is essential that this string already be
  419. initialized to a sufficient length before calling DOS.  Otherwise, the
  420. returned directory name will likely overwrite other existing data.  [And if
  421. that data happens to be a BASIC string descriptor or back pointer you will
  422. likely crash the program and possibly even have to reboot the PC.]
  423.      When a string is sent as a parameter to a DOS routine, it must be
  424. terminated with a CHR$(0), so DOS can tell where it ends.  Likewise, when
  425. DOS returns a string to your program such as the current directory, it
  426. indicates the end with a CHR$(0).  Therefore, it is up to your program to
  427. manually append a CHR$(0) to any file or directory names you pass to DOS. 
  428. And when receiving a string from DOS, you must use INSTR to locate the
  429. CHR$(0) that marks the end, and keep only what precedes that character.
  430.      I will start with some simple examples that access DOS Interrupt &H21,
  431. and proceed to more complex routines that pass and receive string data.
  432.  
  433.  
  434. ACCESSING THE DEFAULT DRIVE
  435.  
  436. The first DOS example shows how to determine the current default drive, and
  437. it is designed as a DEF FN-style function.  A function is a natural way to
  438. design a routine that returns information, as opposed to a called
  439. subprogram.  Further, using a DEF FN-style function reduces the amount of
  440. code that BASIC generates, and also reduces the code needed each time the
  441. function is invoked.
  442.  
  443.  
  444. DEFINT A-Z
  445.  
  446. '$INCLUDE: 'REGTYPE.BI'
  447. DIM Registers AS RegType
  448.  
  449. DEF FnGetDrive%
  450.   Registers.AX = &H1900
  451.   CALL Interrupt(&H21, Registers, Registers)
  452.   FnGetDrive% = (Registers.AX AND &HFF) + 65
  453. END DEF
  454.  
  455. PRINT "The current default drive is "; CHR$(FnGetDrive%)
  456.  
  457.  
  458. Here, service number &H19 is assigned to the AH portion of AX prior to
  459. calling Interrupt &H21, and the value that DOS returns in AL indicates the
  460. current drive.  For this service DOS uses 0 to indicate drive A, 1 for
  461. drive B, and so forth.  Therefore, you use AND with the value &HFF (255) to
  462. keep just the low portion in AX.  Once the DOS drive number has been
  463. isolated, the program adds 65 to adjust that to the equivalent ASCII
  464. character value.
  465.      Setting a new default drive is just as easy as obtaining the current
  466. drive.  Although BASIC PDS provides the CHDRIVE command to set a new drive
  467. as the current default, QuickBASIC does not.  The ChDrive subprogram that
  468. follows affords the same functionality to QuickBASIC users, and it accepts
  469. a single letter to indicate which drive is to be made the new current
  470. default.
  471.  
  472.  
  473. DEFINT A-Z
  474. DECLARE SUB ChDrive (Drive$)
  475.  
  476. '$INCLUDE: 'REGTYPE.BI'
  477.  
  478. DIM SHARED Registers AS RegType
  479.  
  480. INPUT "Enter the drive to make current: ", NewDrive$
  481. CALL ChDrive(NewDrive$)
  482.  
  483. SUB ChDrive (Drive$) STATIC
  484.   Registers.AX = &HE00
  485.   Registers.DX = ASC(UCASE$(Drive$)) - 65
  486.   CALL Interrupt(&H21, Registers, Registers)
  487. END SUB
  488.  
  489.  
  490. Now that you know how to set and get the current default drive, you can
  491. combine the two and create a function that tells if a given drive letter is
  492. valid.  Many DOS services return the success or failure of an operation
  493. using the CPU's Carry flag.  However, the service that sets a new drive is
  494. a notable exception.  Therefore, to determine if a given drive letter is in
  495. fact valid requires more than simply trying to set the new drive, and then
  496. seeing if an error resulted.
  497.      The only way to tell if a request to change the current drive was
  498. accepted is to make another call to get the current drive, thereby seeing
  499. if the original request took effect.  The program that follows accepts a
  500. drive letter as a string, and returns True or False (-1 or 0) to indicate
  501. whether or not the drive is valid.
  502.  
  503. DEFINT A-Z
  504. DECLARE SUB ChDrive (Drive$)
  505.  
  506. '$INCLUDE: 'REGTYPE.BI'
  507.  
  508. DIM SHARED Registers AS RegType
  509.  
  510. DEF FnGetDrive%
  511.   Registers.AX = &H1900
  512.   CALL Interrupt(&H21, Registers, Registers)
  513.   FnGetDrive% = (Registers.AX AND &HFF) + 65
  514. END DEF
  515.  
  516. DEF FnDriveValid% (TestDrive$)
  517.   STATIC Current                'local to this function
  518.   Current = FnGetDrive%         'save the current drive
  519.   FnDriveValid% = 0             'assume not valid
  520.   CALL ChDrive(TestDrive$)      'try to set a new drive
  521.   IF ASC(UCASE$(TestDrive$)) = FnGetDrive% THEN
  522.      FnDriveValid% = -1         'they match so it's valid
  523.   END IF
  524.   CALL ChDrive(CHR$(Current))   'either way restore it
  525. END DEF
  526.  
  527. INPUT "Enter the drive to test for validity: ", Drive$
  528. IF FnDriveValid%(Drive$) THEN
  529.    PRINT Drive$; " is a valid drive."
  530. ELSE
  531.    PRINT "Sorry, drive "; Drive$; " is not valid."
  532. END IF
  533.  
  534. SUB ChDrive (Drive$) STATIC
  535.   Registers.AX = &HE00
  536.   Registers.DX = ASC(UCASE$(Drive$)) - 65
  537.   CALL Interrupt(&H21, Registers, Registers)
  538. END SUB
  539.  
  540. The strategy used here is to first save the current default drive, and then
  541. set a new drive on a trial basis.  If the current drive is the one that was
  542. just set, then the specified drive was indeed valid.  In either case, the
  543. original drive must be restored.
  544.  
  545.  
  546. DETERMINING IF A FILE EXISTS
  547.  
  548. Both of the DOS services we have considered so far use integer arguments to
  549. indicate the new drive, or which drive is the current default.  The next
  550. example shows how to pass a BASIC string to a DOS service, which is
  551. somewhat more complicated.  The situation is made worse by the far strings
  552. feature available in BASIC PDS.  Therefore, be sure to observe the comment
  553. that shows how to replace SSEG with VARSEG for use with QuickBASIC.
  554.      Chapter 6 showed an admittedly clunky way to determine if a file is
  555. present.  The example given there attempted to open the specified file for
  556. random access, and then used LOF to see if the file had a length of zero. 
  557. The problem with that method--besides requiring a lot of unnecessary DOS
  558. activity--is that it reports a file with a perfectly legal length of zero
  559. as not being present, and then deletes it!
  560.      The FnFileExist function that follows is intended for use with BASIC
  561. PDS, and comments show how to change it for use with QuickBASIC.  Please
  562. understand that PDS doesn't really need a File Exist function, since DIR$
  563. can be used for that purpose.  The statement IF LEN(DIR$(FileSpec$)) THEN
  564. will quickly tell if a file is present.  However, the point is to show how
  565. strings are passed to DOS, and for that purpose this example serves quite
  566. nicely.
  567.  
  568. DEFINT A-Z
  569. '$INCLUDE: 'REGTYPE.BI'
  570.  
  571. DIM Registers AS RegType
  572.  
  573. TYPE DTA                         'used by DOS services
  574.   Reserved  AS STRING * 21       'reserved for use by DOS
  575.   Attribute AS STRING * 1        'the file's attribute
  576.   FileTime  AS STRING * 2        'the file's time
  577.   FileDate  AS STRING * 2        'the file's date
  578.   FileSize  AS LONG              'the file's size
  579.   FileName  AS STRING * 13       'the file's name
  580. END TYPE
  581. DIM DTAData AS DTA
  582.  
  583. DEF FnFileExist% (Spec$)
  584.   FnFileExist% = -1              'assume the file exists
  585.  
  586.   Registers.DX = VARPTR(DTAData) 'set a new DOS DTA
  587.   Registers.DS = VARSEG(DTAData)
  588.   Registers.AX = &H1A00
  589.   CALL InterruptX(&H21, Registers, Registers)
  590.  
  591.   Spec$ = Spec$ + CHR$(0)      'DOS needs an ASCIIZ string
  592.   Registers.AX = &H4E00        'find file name service
  593.   Registers.CX = 39            'attribute for any file
  594.   Registers.DX = SADD(Spec$)   'show where the spec is
  595.   Registers.DS = SSEG(Spec$)   'use this with BASIC PDS
  596.  'Registers.DS = VARSEG(Spec$) 'use this with QuickBASIC
  597.  
  598.   CALL InterruptX(&H21, Registers, Registers)
  599.   IF Registers.Flags AND 1 THEN FnFileExist% = 0
  600. END DEF
  601.  
  602. INPUT "Enter a file name or specification: ", FileSpec$
  603. IF FnFileExist%(FileSpec$) THEN
  604.    PRINT FileSpec$; " does exist"
  605. ELSE
  606.    PRINT "Sorry, no files match "; FileSpec$
  607. END IF
  608.  
  609. FnFileExist calls upon the DOS Find First service that searches a directory
  610. and attempts to locate the first file that matches a given specification
  611. template.  Therefore, besides being able to see if ACCOUNTS.DAT or
  612. F:\UTILS\NU.EXE exist, you can also use the DOS wild cards.   For example,
  613. given C:\QB45\*.BAS, FnFileExist will report if any files with a .BAS
  614. extension are in the \QB45 directory of drive C.
  615.      As part of its directory searching mechanism, DOS requires a block of
  616. memory known as a Disk Transfer Area, or DTA for short.  If a matching file
  617. name is found, DOS stores important information about the file there, where
  618. your program can read it.  As you can see by examining the DTAType
  619. structure, this includes the file's name and extension, the date and time
  620. it was last written, to, its current size, and attribute.  The 21-byte
  621. string at the beginning identified as Reserved holds sector numbers and
  622. other information, and is used by DOS for subsequent searches.  This
  623. function doesn't use any of the information in the DTA; however, it must
  624. still be defined for use by DOS.
  625.      You will notice that FnFileExist uses the InterruptX routine rather
  626. than Interrupt, and this is to provide support for use with BASIC PDS far
  627. strings.  Two of the CPU's registers are used to hold the DS and ES data
  628. segment registers.  When Interrupt is called, it simply leaves whatever is
  629. currently in DS and ES and then calls the interrupt.  InterruptX, on the
  630. other hand, loads DS and ES from those components of the Registers TYPE
  631. variable, and those are the values the interrupt itself receives.  Were
  632. FnFileExist limited to working with QuickBASIC [where all strings are in
  633. the DS segment], Interrupt would be sufficient and the added complication
  634. of using either VARSEG or SSEG could be avoided.
  635.      Note that InterruptX can also be told to use the current value of DS
  636. for both DS and ES, when the calling program doesn't need or want to change
  637. them.  This is specified by placing a value of -1 into either or both
  638. portions of the Registers TYPE variable.  For example, the statement
  639. Registers.DS = -1 tells InterruptX not to assign DS before performing the
  640. interrupt.  Otherwise, if Registers.DS were not assigned, DS would receive
  641. the value 0 which is incorrect for DOS services that receive a variable's
  642. address.  In a similar manner, Registers.ES = -1 tells InterruptX to set ES
  643. to the current value of DS.
  644.  
  645.  
  646. THE CARRY FLAG
  647.  
  648. The last item to note in this function is how the Carry flag is tested.  As
  649. I mentioned earlier, many DOS services indicate the success or failure of
  650. an operation by either clearing or setting the CPU's Carry flag.  This flag
  651. is held in one bit in the Flags register, and its primary purpose is to
  652. assist multi-word arithmetic in assembly language programs.  But because
  653. the 80x86 provides single instructions that easily set and test this flag,
  654. the designers of DOS decided to use it as an error indicator.
  655.      The Carry flag is stored in the lowest bit of the Flags register, and
  656. can therefore be tested using the AND instruction with a value of 1.  If
  657. that bit is set, the result of the AND test will be one; otherwise it will
  658. be zero.  Thus, the statement IF Registers.Flags AND 1 THEN will be true if
  659. the Carry flag is set, which indicates an error.  In the case of DOS' Find
  660. First function this is not really an error in the strictest sense.  But
  661. there is no need here to distinguish between, say, an invalid path name and
  662. the lack of any matching files.  Either a match was found or it wasn't.
  663.  
  664.  
  665. IMPROVING ON INTERRUPT
  666. ======================
  667.  
  668. Recall that Chapter 8 introduced the DOSInt routine which serves as a
  669. small-code replacement for BASIC's InterruptX routine.  Although the
  670. reduction in code size gained by using DOSInt versus Interrupt or
  671. InterruptX is not dramatic, it can save several hundred bytes in a program
  672. that calls it many times.  DOSInt is also somewhat easier to set up and
  673. use, because it requires only a single Registers argument.
  674.      Of course, DOSInt is meant only for use with DOS Interrupt &H21, and
  675. it will not work with any other DOS or BIOS interrupt services.  Because of
  676. the savings that DOSInt affords, the remaining DOS examples in this chapter
  677. will use DOSInt instead of Interrupt or InterruptX.  Like InterruptX,
  678. DOSInt lets you access the DS and ES registers, and it also recognizes an
  679. incoming value of -1 to specify the current contents of DS.
  680.  
  681.  
  682. OBTAINING THE CURRENT DIRECTORY
  683.  
  684. Where FnFileExist shows how to pass a BASIC string to a DOS interrupt
  685. service, the FnGetDir function following shows how to receive a string from
  686. DOS.  Again, BASIC PDS users have the CURDIR$ function which reports the
  687. current directory, but most QuickBASIC programmers will find this function
  688. invaluable.
  689.  
  690. DEFINT A-Z
  691. '$INCLUDE: 'REGTYPE.BI'
  692.  
  693. DIM Registers AS RegType
  694.  
  695. DEF FnGetDir$ (Drive$)
  696.   STATIC Temp$, Drive, Zero     'local variables
  697.  
  698.   IF LEN(Drive$) THEN           'did they pass a drive?
  699.     Drive = ASC(UCASE$(Drive$)) - 64
  700.   ELSE
  701.     Drive = 0
  702.   END IF
  703.  
  704.   Temp$ = SPACE$(65)            'DOS stores the name here
  705.  
  706.   Registers.AX = &H4700         'get directory service
  707.   Registers.DX = Drive          'the drive goes in DL
  708.   Registers.SI = SADD(Temp$)    'show DOS where Temp$ is
  709.   Registers.DS = SSEG(Temp$)    'use this with BASIC PDS
  710.  'Registers.DS = -1             'use this with QuickBASIC
  711.  
  712.   CALL DOSInt(Registers)        'call DOS
  713.  
  714.   IF Registers.Flags AND 1 THEN 'must be an invalid drive
  715.     FnGetDir$ = ""
  716.   ELSE
  717.     Zero = INSTR(Temp$, CHR$(0))    'find the zero byte
  718.     FnGetDir$ = "\" + LEFT$(Temp$, Zero)
  719.   END IF
  720. END DEF
  721.  
  722. PRINT "Which drive? ";
  723. DO
  724.   Drive$ = INKEY$
  725. LOOP UNTIL LEN(Drive$)
  726. PRINT
  727.  
  728. Cur$ = FnGetDir$(Drive$)
  729. IF LEN(Cur$) THEN
  730.   PRINT "The current directory is ";
  731.   PRINT Drive$; ":"; FnGetDir$(Drive$)
  732. ELSE
  733.   PRINT "Invalid drive"
  734. END IF
  735.  
  736. PRINT "The current directory for the default drive is ";
  737. PRINT FnGetDir$("")
  738.  
  739. The variables Temp$, Drive, and Zero are declared as STATIC to prevent them
  740. from conflicting with variables of the same name in your program.  Of
  741. course, you could convert this to a formal FUNCTION procedure if you
  742. prefer, which considers variables local by default.  Converting to a formal
  743. function is also needed if you plan to access it from multiple source
  744. modules.
  745.      Unlike the DOS Get Drive and Set Drive services, service &H47 uses a
  746. value of one to indicate drive A, 2 for drive B, and so forth.  To request
  747. the current directory on the default drive you must use a value of zero. 
  748. An explicit test for this is made at the beginning of the function.  Later,
  749. this value is assigned to Registers.DX where DOS expects it.  Note that it
  750. is really DL that will hold the specified drive number.  But assigning DX
  751. from Drive as shown does this, and also clears the high (DH) portion in the
  752. process.  Since the contents of DH are ignored by this DOS service, no harm
  753. is done and the extra code that would be needed to assign only DL can be
  754. avoided.
  755.      As I mentioned earlier, it is essential that you set aside space to
  756. hold the returned directory name.  Since the longest path name that DOS can
  757. accommodate is 65 characters, Temp$ is assigned to that length.  Then, the
  758. segment and address where Temp$ is stored are passed to DOS in the DS and
  759. SI registers.  Note that DOS is not very consistent in its use of
  760. registers.  Where the service that finds the first matching file name uses
  761. DS:DX to point to the file specification, this service uses DS:SI to point
  762. to the string.
  763.      Like the FnFileExist function, you must change the statement that
  764. assigns Registers.DS if you plan to use this one with QuickBASIC.  The
  765. BASIC PDS version of that statement is left active rather than the
  766. QuickBASIC version, so QuickBASIC will highlight that line as an error to
  767. remind you.  Although FnFileExist uses VARSEG for the DS value when used
  768. with QuickBASIC, FnGetDir uses -1.  Both methods work, and I used -1 here
  769. just to show that in context.
  770.      After DOSInt is called to load Temp$ with the current directory name,
  771. the Carry Flag is tested to see if an error occurred.  The only error that
  772. is possible here is "Invalid drive", in which case FnGetDir$ is assigned a
  773. null value as a flag to indicate that.  Otherwise, INSTR is used to locate
  774. the CHR$(0) zero byte that DOS assigned to mark the end of the name.
  775.      This error testing can be left out to save code if you prefer.  You
  776. could also validate the drive using the FnDriveValid function, either by
  777. adding the code within FnGetDir, or separately prior to invoking it.
  778.  
  779.  
  780. READING FILE AND DIRECTORY NAMES
  781.  
  782. One important service that many programs need and which BASIC has never
  783. provided is the ability to read directory names from disk.  Any word
  784. processor worth its salt will let you view a list of files that match, say,
  785. a *.DOC extension, and then select the one you want to edit.  With the
  786. introduction of BASIC PDS Microsoft added the DIR$ function, which lets you
  787. read file names.  However, there is no way to specify file attributes
  788. (hidden, read-only, and so forth), and also no way to read directory names. 
  789. To add insult to injury, the PDS manuals do not show clearly how to read a
  790. list of file names, and store them into a string array.
  791.      The program that follows counts the number of files or directories
  792. that match a given specification, and then dimensions and loads a string
  793. array with their names.
  794.  
  795. DEFINT A-Z
  796. DECLARE SUB LoadNames (FileSpec$, Array$(), Attribute%)
  797.  
  798. '$INCLUDE: 'REGTYPE.BI'
  799.  
  800. TYPE DTA                        'used by find first/next
  801.   Reserved  AS STRING * 21      'reserved for use by DOS
  802.   Attribute AS STRING * 1       'the file's attribute
  803.   FileTime  AS STRING * 2       'the file's time
  804.   FileDate  AS STRING * 2       'the file's date
  805.   FileSize  AS LONG             'the file's size
  806.   FileName  AS STRING * 13      'the file's name
  807. END TYPE
  808.  
  809. DIM SHARED DTAData AS DTA       'shared so LoadNames can
  810. DIM SHARED Registers AS RegType '  access them too
  811.  
  812.  
  813. DEF FnFileCount% (Spec$, Attribute)
  814.   STATIC Count                   'make this private
  815.  
  816.   Registers.DX = VARPTR(DTAData) 'set new DTA address
  817.   Registers.DS = -1              'the DTA is in DGROUP
  818.   Registers.AX = &H1A00          'specify service 1Ah
  819.   CALL DOSInt(Registers)         'DOS set DTA service
  820.  
  821.   Count = 0                      'clear the counter
  822.   Spec$ = Spec$ + CHR$(0)        'make an ASCIIZ string
  823.   IF Attribute AND 16 THEN       'find directory names?
  824.     DirFlag = -1                 'yes
  825.   ELSE
  826.     DirFlag = 0                  'no
  827.   END IF
  828.  
  829.   Registers.DX = SADD(Spec$)     'the file spec address
  830.   Registers.DS = SSEG(Spec$)     'this is for BASIC PDS
  831.  'Registers.DS = -1              'this is for QuickBASIC
  832.   Registers.CX = Attribute       'assign the attribute
  833.   Registers.AX = &H4E00          'find first matching name
  834.  
  835.   DO
  836.     CALL DOSInt(Registers)       'see if there's a match
  837.     IF Registers.Flags AND 1 THEN EXIT DO   'no more
  838.     IF DirFlag THEN
  839.       IF ASC(DTAData.Attribute) AND 16 THEN
  840.         IF LEFT$(DTAData.FileName, 1) <> "." THEN
  841.           Count = Count + 1      'increment the counter
  842.         END IF
  843.       END IF
  844.     ELSE
  845.       Count = Count + 1          'they want regular files
  846.     END IF
  847.  
  848.     Registers.AX = &H4F00        'find next name
  849.   LOOP
  850.  
  851.   FnFileCount% = Count           'assign the function
  852. END DEF
  853.  
  854.  
  855. REDIM Names$(1 TO 1)             'create a dynamic array
  856. Attribute = 19                   'matches directories only
  857. Attribute = 39                   'matches all files
  858.  
  859. INPUT "Enter a file specification: ", Spec$
  860. CALL LoadNames(Spec$, Names$(), Attribute)
  861.  
  862. FOR X = LEN(Spec$) TO 1 STEP -1  'isolate the drive/path
  863.   Temp = ASC(MID$(Spec$, X, 1))
  864.   IF Temp = 58 OR Temp = 92 THEN '":" or "\"
  865.     Path$ = LEFT$(Spec$, X)      'keep what precedes that
  866.     EXIT FOR                     'and we're all done
  867.   END IF
  868. NEXT
  869.  
  870. FOR X = 1 TO UBOUND(Names$)      'print the names
  871.   PRINT Path$; Names$(X)
  872. NEXT
  873.  
  874. PRINT
  875. PRINT UBOUND(Names$); "matching file(s)"
  876. END
  877.  
  878.  
  879. SUB LoadNames (FileSpec$, Array$(), Attribute) STATIC
  880.  
  881.   Spec$ = FileSpec$ + CHR$(0)     'make an ASCIIZ string
  882.   NumFiles = FnFileCount%(Spec$, Attribute) 'count names
  883.   IF NumFiles = 0 THEN EXIT SUB             'exit if none
  884.   REDIM Array$(1 TO NumFiles)    'dimension the array
  885.  
  886.   IF Attribute AND 16 THEN       'find directory names?
  887.     DirFlag = -1                 'yes
  888.   ELSE
  889.     DirFlag = 0                  'no
  890.   END IF
  891.  
  892.   '---- The following code isn't strictly necessary
  893.   '     because we know that FnFileCount already set the
  894.   '     DTA address.
  895.  'Registers.DX = VARPTR(DTAData) 'set new DTA address
  896.  'Registers.DS = -1              'the DTA in DGROUP
  897.  'Registers.AX = &H1A00          'specify service 1Ah
  898.  'CALL DOSInt(Registers)         'DOS set DTA service
  899.  
  900.   Registers.DX = SADD(Spec$)     'the file spec address
  901.   Registers.DS = SSEG(Spec$)     'this is for BASIC PDS
  902.  'Registers.DS = -1              'this is for QuickBASIC
  903.   Registers.CX = Attribute       'assign the attribute
  904.   Registers.AX = &H4E00          'find first matching name
  905.   Count = 0                      'clear the counter
  906.  
  907.   DO
  908.     CALL DOSInt(Registers)       'see if there's a match
  909.     IF Registers.Flags AND 1 THEN EXIT DO   'no more
  910.     Valid = 0
  911.     IF DirFlag THEN                         'directories?
  912.       IF ASC(DTAData.Attribute) AND 16 THEN
  913.         IF LEFT$(DTAData.FileName, 1) <> "." THEN
  914.           Valid = -1             'this name is valid
  915.         END IF
  916.       END IF
  917.     ELSE
  918.       Valid = -1                 'they want regular files
  919.     END IF
  920.  
  921.     IF Valid THEN                'process the file if it
  922.       Count = Count + 1          '  passed all the tests
  923.       Zero = INSTR(DTAData.FileName, CHR$(0))
  924.       Array$(Count) = LEFT$(DTAData.FileName, Zero - 1)
  925.     END IF
  926.     Registers.AX = &H4F00        'find next matching name
  927.   LOOP
  928.  
  929. END SUB
  930.  
  931. These routines call upon the DOS Find First and Find Next services, which
  932. performs the actual searching and loading of the names.  Before the names
  933. can be loaded into an array, you need some way to know how many files there
  934. are.  Therefore, the FnFileCount function makes repeated calls to DOS to
  935. find another file, until there are no more.
  936.      The general strategy is to request service &H4E to find the first
  937. matching file.  If a file is found then the Carry Flag is returned clear;
  938. otherwise it is set and the function returns with a count of zero.  If a
  939. file is found Registers.AX is assigned a value of &H4F, and this tells DOS
  940. to resume searching based on the same file specification as before.  Where
  941. the FnFileExist function merely needed to check for the presence of a file
  942. using the Find First service, this one continues in a DO loop until no more
  943. matching files are found.
  944.      Understand that these DOS services accept either a partial file
  945. specification such as "*.BAS" or "D:\PATHNAME\*.*", or a single file name
  946. such as "CONFIG.SYS" or "C:\AUTOEXEC.BAT".
  947.  
  948.  
  949. File Attributes
  950.  
  951. The DOS Find services also accept--and require--a file attribute indicating
  952. the type of files that are being sought.  The method of specifying and
  953. isolating files and their attributes is convoluted and confusing to be
  954. sure.  Figure 11-3 lists each of the six file attributes, and shows which
  955. corresponds to each bit in the attribute byte.
  956.  
  957.  
  958.  7   6   5   4   3   2   1   0  <── Bits
  959. 128  64  32  16  8   4   2   1  <── Numeric Values
  960. ═══ ═══ ═══ ═══ ═══ ═══ ═══ ═══
  961.  │   │   │   │   │   │   │   │
  962.  │   │   │   │   │   │   │   └───── Read-Only
  963.  │   │   │   │   │   │   └───────── Hidden
  964.  │   │   │   │   │   └───────────── System
  965.  │   │   │   │   └───────────────── Volume Label
  966.  │   │   │   └───────────────────── Subdirectory
  967.  │   │   └───────────────────────── Archive
  968.  └───┴───────────────────────────── Unused
  969.  
  970. Figure 11-3: The makeup of the bits in the attribute byte, and the
  971. individual decimal value of each.
  972.  
  973.  
  974. In most cases, the attribute bits are cumulative.  For example, if you
  975. specify that you want to locate files marked as read-only, you will also
  976. get files that are not.  But if you leave that bit clear, then read-only
  977. files will not be included.  The same logic is used for reading directory
  978. names.  If the directory bit is set then you will read directories, and
  979. also regular files whose directory bit is not set.  This requires that you
  980. perform additional qualifications when the file name is read into the DTA. 
  981. To make matters even worse, there is an exception to this rule whereby an
  982. attribute of zero will still read file names whose archive bit is set.
  983.      Before considering how to qualify the names as they are read, you must
  984. first understand what attributes are and how to specify them to begin with. 
  985. Every file has an attribute, which is set by DOS to Archive at the time it
  986. is created.  The archive bit is used solely to tell if the file has been
  987. backed up using the DOS BACKUP utility.  When BACKUP copies the file to a
  988. floppy disk, it clears the Archive bit in the file's directory entry.  Then
  989. if the file is written to again later, DOS sets that bit.  This way, BACKUP
  990. can tell which files need to be backed up, and which ones haven't changed
  991. since the last backup was performed.  Most modern commercial backup
  992. utilities also manipulate the archive bit, for the same reason that DOS'
  993. BACKUP does.
  994.      The hidden bit tells the DOS DIR command not to display that file's
  995. name.  Although it won't display in a directory listing, a hidden file may
  996. be opened, read from, and written to.  The system bit is similar in that it
  997. also tells DIR not to display the file.  The IO.SYS and MSDOS.SYS files
  998. that come with MS-DOS are hidden system files, so to read their names you
  999. must set those bits in the search attribute.  Note that IBM's version of
  1000. DOS uses the names IBMBIO.COM and IBMDOS.COM respectively for the same
  1001. files.
  1002.      The label bit identifies a file as the disk's volume label, which
  1003. isn't really a file at all.  Every disk is allowed to have one volume label
  1004. entry in its root directory, which lets an application identify the disk. 
  1005. This feature is not particularly important with hard disks, but when
  1006. floppy-only systems were the norm this let programs ensure that the correct
  1007. data diskette was installed in the drive.  Even though a volume label is
  1008. stored in the disk's directory like a regular file name, no sectors are
  1009. allocated to it.  Note that a bug in DOS 2.x versions causes a search for a
  1010. volume label to fail.  The only work-around is to use the more complex DOS
  1011. 1.x Find First/Next services that are still supported in later versions for
  1012. compatibility with older programs.
  1013.      Finally, the subdirectory attribute bit identifies a file as a
  1014. directory.  From DOS' perspective a subdirectory *is* a file, with fixed-
  1015. length records that hold the names, attributes, and other information for
  1016. the files it contains.  Notice that the "." and ".." directory entries that
  1017. appear when you type DIR are in fact present in that directory.
  1018.      Every directory except the root contains these entries, and they also
  1019. have a directory attribute.  The single dot refers to the current
  1020. directory, and the double dots to the parent directory one level above.  I
  1021. mention this because these "dot" entries are reported by the Find First and
  1022. Find Next services, and in many cases you will want to filter them out.
  1023.      To specify a file attribute you must determine the correct value,
  1024. based on the individual bits to be included in the search.  As I stated
  1025. earlier, setting the attribute to zero includes all normal files, and
  1026. exclude any marked as read-only, hidden, system, or subdirectory. 
  1027. Therefore, to include all files but not subdirectories you will use an
  1028. attribute value of 39.  This value is derived by adding up the bit values
  1029. for each desired attribute as shown in Figure 11-3.
  1030.      When you add all of the values for each bit of interest, the answer is
  1031. 32 (archive) + 4 (system) + 2 (hidden) + 1 (read-only) = 39.  In a similar
  1032. fashion, you will use 16 to read directory names, but hidden or read-only
  1033. directories will not be included unless you also add 2 + 1 = 3, resulting
  1034. in a final value of 19.
  1035.      Although you can specify attribute bits in nearly any combination, DOS
  1036. returns all of the names that match any of the bits.  Therefore, you must
  1037. further qualify the files by examining the attribute DOS returns in the DTA
  1038. TYPE variable.  A typical search for directory names will ask to include
  1039. all three attribute bits (directory, hidden, and read-only), but the
  1040. qualification test merely tests if the directory bit is set.  The following
  1041. excerpt shows this in context.
  1042.  
  1043.      Registers.CX = 19
  1044.      CALL DOSInt(Registers)
  1045.      IF ASC(DTAData.Attribute) AND 16 THEN  'it is a directory
  1046.  
  1047. Even if the directory was in fact hidden or read-only, the test for the
  1048. directory bit will succeed regardless of any other bits that may be set. 
  1049. Unfortunately, the reverse is not true.  If the directory is not hidden or
  1050. read-only, then testing for those bits will fail.  Both the FnFileCount
  1051. function and the LoadNames subprogram include an explicit test for
  1052. directory searches, and contain additional logic to check for this case.
  1053.      You could also add similar logic to the FnFileExist function, or
  1054. create a separate version perhaps called FnDirExist that adds a test for
  1055. the directory bit and also filters out the "dot" entries.
  1056.  
  1057.  
  1058. REDIM PRESERVE
  1059.  
  1060. One glaring shortcoming you have probably already noticed is the enormous
  1061. amount of code that is duplicated in both the FnFileCount and LoadNames
  1062. routines.  In fact, the two are almost identical, except that LoadNames
  1063. also assigns elements in the array.  Worse, having to count all of the
  1064. names before they can be read greatly increases the amount of time needed
  1065. to process a directory when there are many files.  Until you know how many
  1066. files are present, there's no way to known how large to dimension the
  1067. string array.
  1068.      One solution is to create an array with, say, 500 elements, and hope
  1069. that the actual number of files does not exceed that.  But if there are
  1070. only a few files this wastes a lot of memory, and when there are more than
  1071. 500, then, well, you're still out of luck.  In fact, this is one of the few
  1072. features that C offers but QuickBASIC does not.  C programs can allocate
  1073. memory that will be treated as an array, and then repeatedly request more
  1074. memory for that same array as it is needed.
  1075.      Fortunately, BASIC PDS version 7.1 includes the PRESERVE option to the
  1076. REDIM statement.  This allows you to increase (or decrease) the size of an
  1077. array, but without destroying its current contents.  Thus, REDIM PRESERVE
  1078. is ideal for applications like this that require an array's size to be
  1079. altered.  The next, much shorter program uses REDIM PRESERVE to advantage,
  1080. and avoids the extra step that counts how many files match the search
  1081. specification.  Of course, this program requires BASIC PDS.
  1082.  
  1083. DEFINT A-Z
  1084. DECLARE SUB LoadNames (FileSpec$, Array$(), Attribute%)
  1085.  
  1086. '$INCLUDE: 'REGTYPE.BI'
  1087.  
  1088. TYPE DTA                        'used by find first/next
  1089.   Reserved  AS STRING * 21      'reserved for use by DOS
  1090.   Attribute AS STRING * 1       'the file's attribute
  1091.   FileTime  AS STRING * 2       'the file's time
  1092.   FileDate  AS STRING * 2       'the file's date
  1093.   FileSize  AS LONG             'the file's size
  1094.   FileName  AS STRING * 13      'the file's name
  1095. END TYPE
  1096.  
  1097. DIM SHARED DTAData AS DTA       'shared so LoadNames can
  1098. DIM SHARED Registers AS RegType '  access them too
  1099.  
  1100. REDIM Names$(1 TO 1)             'create a dynamic array
  1101. Attribute = 19                   'matches directories only
  1102. Attribute = 39                   'matches all files
  1103. Spec$ = "*.*"                    'so does this
  1104. CALL LoadNames(Spec$, Names$(), Attribute)
  1105.  
  1106. IF Names$(1) = "" THEN           'check for no files
  1107.   PRINT "No matching files"
  1108. ELSE
  1109.   FOR X = 1 TO UBOUND(Names$)    'print the names
  1110.     PRINT Path$; Names$(X)
  1111.   NEXT
  1112. END IF
  1113. END
  1114.  
  1115.  
  1116. SUB LoadNames (FileSpec$, Array$(), Attribute) STATIC
  1117.   Spec$ = FileSpec$ + CHR$(0)    'make an ASCIIZ string
  1118.   Count = 0                      'clear the counter
  1119.  
  1120.   Registers.DX = VARPTR(DTAData) 'set new DTA address
  1121.   Registers.DS = -1              'the DTA is in DGROUP
  1122.   Registers.AX = &H1A00          'specify service 1Ah
  1123.   CALL DOSInt(Registers)         'DOS set DTA service
  1124.  
  1125.   IF Attribute AND 16 THEN       'find directory names?
  1126.     DirFlag = -1                 'yes
  1127.   ELSE
  1128.     DirFlag = 0                  'no
  1129.   END IF
  1130.  
  1131.   Registers.DX = SADD(Spec$)     'the file spec address
  1132.   Registers.DS = SSEG(Spec$)     'this is for BASIC PDS
  1133.   Registers.CX = Attribute       'assign the attribute
  1134.   Registers.AX = &H4E00          'find first matching name
  1135.  
  1136.   DO
  1137.     CALL DOSInt(Registers)       'see if there's a match
  1138.     IF Registers.Flags AND 1 THEN EXIT DO   'no more
  1139.  
  1140.     Valid = 0                    'invalid until qualified
  1141.     IF DirFlag THEN              'find directories?
  1142.       IF ASC(DTAData.Attribute) AND 16 THEN 'yes, is it?
  1143.         IF LEFT$(DTAData.FileName, 1) <> "." THEN
  1144.           Valid = -1             'this name is valid
  1145.         END IF
  1146.       END IF
  1147.     ELSE
  1148.       Valid = -1                 'they want regular files
  1149.     END IF
  1150.  
  1151.     IF Valid THEN                'process the file if it
  1152.       Count = Count + 1          '  passed all the tests
  1153.       REDIM PRESERVE Array$(1 TO Count)  'expand the array
  1154.       Zero = INSTR(DTAData.FileName, CHR$(0)) 'find zero
  1155.       Array$(Count) = LEFT$(DTAData.FileName, Zero - 1)
  1156.     END IF
  1157.  
  1158.     Registers.AX = &H4F00        'find next matching name
  1159.   LOOP
  1160. END SUB
  1161.  
  1162. MANAGING FILES
  1163.  
  1164. Chapter 6 explained in great detail how files are opened, closed, read, and
  1165. written using BASIC.  I mentioned there that BASIC imposes a number of
  1166. arbitrary limitations on what you can and cannot do with files.  Indeed,
  1167. DOS allows almost any action except writing to a file that has been opened
  1168. for input.  As you can imagine, CALL Interrupt--or in this case the DOSInt
  1169. replacement routine--can be used to circumvent BASIC and access your files
  1170. directly.
  1171.      Although BASIC expects you to state how the file will be accessed with
  1172. the various OPEN options, to DOS all files are considered as being opened
  1173. for binary access.  There is no equivalent DOS service for BASIC's INPUT #
  1174. or PRINT # commands. Therefore, it is up to you to write subroutines that
  1175. look for a terminating carriage return and optional line feed when reading
  1176. sequential text.  Likewise, it is up to you to manually append a carriage
  1177. return and line feed to the end of each line of text written to disk.
  1178.      Frankly, sequential file access is often best left to BASIC, since a
  1179. lot of time-consuming tests are needed when reading sequential data.  You
  1180. could, however, use the BufIn function shown in Chapter 6, or similar logic
  1181. of your own devising.  There are many types of file access that can be
  1182. performed using direct DOS calls, and I will show those that are the most
  1183. useful and appropriate here.
  1184.      The program that will follow shortly is a combination demonstration,
  1185. and suite of twelve subprograms and functions that perform most of the
  1186. services necessary for manipulating files.  Subprograms are provided to
  1187. replace BASIC's OPEN, CLOSE, GET, and PUT statements, as well as LOCK and
  1188. UNLOCK, SEEK, and KILL.
  1189.      There are also replacement functions for LOC and LOF, as well as two
  1190. additional subprograms that have no BASIC equivalent.  All of the routines
  1191. use the DOSInt interface routine, and avoid using BASIC's file handling
  1192. statements.  The demonstration is comprised of a series of code blocks that
  1193. exercise each routine showing how it is used.  Comments at the start of
  1194. each block explain what is being demonstrated.
  1195.      One reason to go behind BASIC's back this way is to avoid its many
  1196. restrictions.  For example, BASIC will not let you read from a file that
  1197. has been opened for output, even though DOS considers this to be perfectly
  1198. legal.  Another is to avoid the need for ON ERROR.  As you learned in
  1199. Chapter 3, ON ERROR can make a program run more slowly, and also increase
  1200. its size.  By going directly to DOS you can avoid the burden of ON ERROR,
  1201. which is otherwise needed to prevent your program from terminating if an
  1202. error occurs.  These replacement routines avoid errors such as those caused
  1203. by attempting to open a file that does not exist, or trying to lock a
  1204. network file that has already been locked by someone else.
  1205.      As with some of the other programs in this book that combine a
  1206. demonstration and subroutines, you should make a copy of the file, and then
  1207. delete all of the code in the main portion of the program.  The only lines
  1208. that must not be deleted are the DEFINT, DECLARE, and INCLUDE statements,
  1209. and also the two DIM SHARED statements.  Then, you can load the resultant
  1210. module into the BASIC editor along with your own main application.
  1211.  
  1212. 'DOS.BAS, demonstrates the direct DOS access routines
  1213.  
  1214. DEFINT A-Z
  1215. DECLARE FUNCTION DOSError% ()
  1216. DECLARE FUNCTION ErrMessage$ (ErrNumber)
  1217. DECLARE FUNCTION LocFile& (Handle)
  1218. DECLARE FUNCTION LofFile& (Handle)
  1219. DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
  1220.  
  1221. DECLARE SUB ClipFile (Handle, NewLength&)
  1222. DECLARE SUB CloseFile (Handle)
  1223. DECLARE SUB FlushFile (Handle)
  1224. DECLARE SUB KillFile (FileName$)
  1225. DECLARE SUB LockFile (Handle, Location&, NumBytes&, Action)
  1226. DECLARE SUB OpenFile (FileName$, OpenMethod, Handle)
  1227. DECLARE SUB ReadFile (Handle, Segment, Address, NumBytes)
  1228. DECLARE SUB SeekFile (Handle, Location&, SeekMethod)
  1229. DECLARE SUB WriteFile (Handle, Segment, Address, NumBytes)
  1230.  
  1231.  
  1232. '$INCLUDE: 'REGTYPE.BI'
  1233.  
  1234. DIM SHARED Registers AS RegType 'so all can access it
  1235. DIM SHARED ErrCode              'ditto for the ErrCode
  1236. CRLF$ = CHR$(13) + CHR$(10)     'define this once now
  1237.  
  1238. COLOR 15, 1                     'this makes the DOS
  1239. CLS                             'messages high-intensity
  1240. COLOR 7, 1
  1241.  
  1242.  
  1243. '---- Open the test file we will use.
  1244. FileName$ = "C:\MYFILE.DAT"     'specify the file name
  1245. OpenMethod = 2                  'read/write non-shared
  1246. CALL OpenFile(FileName$, OpenMethod, Handle)
  1247. GOSUB HandleErr
  1248. PRINT FileName$; " successfully opened, handle:"; Handle
  1249.  
  1250.  
  1251. '---- Write a test message string to the file.
  1252. Msg$ = "This is a test message." + CRLF$
  1253. Segment = SSEG(Msg$)            'use this with BASIC PDS
  1254. 'Segment = VARSEG(Msg$)         'use this with QuickBASIC
  1255. Address = SADD(Msg$)
  1256. NumBytes = LEN(Msg$)
  1257. CALL WriteFile(Handle, Segment, Address, NumBytes)
  1258. GOSUB HandleErr
  1259. PRINT "The test message was successfully written."
  1260.  
  1261.  
  1262. '---- Show how to write a numeric value.
  1263. IntData = 1234
  1264. Segment = VARSEG(IntData)
  1265. Address = VARPTR(IntData)
  1266. NumBytes = 2
  1267. CALL WriteFile(Handle, Segment, Address, NumBytes)
  1268. GOSUB HandleErr
  1269. PRINT "The integer variable was successfully written."
  1270.  
  1271.  
  1272. '---- See how large the file is now.
  1273. Length& = LofFile&(Handle)
  1274. GOSUB HandleErr
  1275. PRINT "The file is now"; Length&; "bytes long."
  1276.  
  1277.  
  1278. '---- Seek back to the beginning of the file.
  1279. Location& = 1                   'specify file offset 1
  1280. SeekMethod = 0                  'relative to beginning
  1281. CALL SeekFile(Handle, Location&, SeekMethod)
  1282. GOSUB HandleErr
  1283. PRINT "We successfully seeked back to the beginning."
  1284.  
  1285.  
  1286. '---- Ensure that the Seek worked by seeing where we are.
  1287. CurSeek& = LocFile&(Handle)
  1288. GOSUB HandleErr
  1289. PRINT "The DOS file pointer is now at location"; CurSeek&
  1290.  
  1291.  
  1292. '---- Read the test message back in again.
  1293. Buffer$ = SPACE$(23)            'the length of Msg$
  1294. Segment = SSEG(Buffer$)         'use this with BASIC PDS
  1295. 'Segment = VARSEG(Buffer$)      'use this with QuickBASIC
  1296. Address = SADD(Buffer$)
  1297. NumBytes = LEN(Buffer$)
  1298. CALL ReadFile(Handle, Segment, Address, NumBytes)
  1299. GOSUB HandleErr
  1300. PRINT "Here is the test message: "; Buffer$
  1301.  
  1302.  
  1303. '---- Skip over the CRLF by reading it as an integer.
  1304. Address = VARPTR(Temp)          'read the CRLF into Temp
  1305. Segment = VARSEG(Temp)
  1306. NumBytes = 2
  1307. CALL ReadFile(Handle, Segment, Address, NumBytes)
  1308. GOSUB HandleErr
  1309.  
  1310.  
  1311. '---- Read the integer written earlier, also into Temp.
  1312. Address = VARPTR(Temp)
  1313. Segment = VARSEG(Temp)
  1314. NumBytes = 2
  1315. CALL ReadFile(Handle, Segment, Address, NumBytes)
  1316. GOSUB HandleErr
  1317. PRINT "The integer value just read is:"; Temp
  1318.  
  1319.  
  1320. '---- Append a new string at the end of the file.
  1321. Msg$ = "This is appended to the end of the file." + CRLF$
  1322. Segment = SSEG(Msg$)            'use this with BASIC PDS
  1323. 'Segment = VARSEG(Msg$)         'use this with QuickBASIC
  1324. Address = SADD(Msg$)
  1325. NumBytes = LEN(Msg$)
  1326. CALL WriteFile(Handle, Segment, Address, NumBytes)
  1327. GOSUB HandleErr
  1328. PRINT "The appended message has been written, ";
  1329. PRINT "but it's still in the DOS file buffer."
  1330.  
  1331.  
  1332. '---- Flush the file's DOS buffer to disk.
  1333. CALL FlushFile(Handle)
  1334. GOSUB HandleErr
  1335. PRINT "Now the buffer has been flushed to disk.  ";
  1336. PRINT "Here's the file contents:"
  1337. SHELL "TYPE " + FileName$
  1338.  
  1339.  
  1340. '---- Display the current length of the file again.
  1341. PRINT "Before calling ClipFile the file is now";
  1342. Length& = LofFile&(Handle)
  1343. GOSUB HandleErr
  1344. PRINT Length&; "bytes long."
  1345.  
  1346.  
  1347. '---- Clip the file to be 2 bytes shorter.
  1348. NewLength& = LofFile&(Handle) - 2
  1349. CALL ClipFile(Handle, NewLength&)
  1350. PRINT "The file has been clipped successfully.  ";
  1351.  
  1352.  
  1353. '---- Prove that the clipping worked successfully.
  1354. Length& = LofFile&(Handle)
  1355. GOSUB HandleErr
  1356. PRINT "It is now"; Length&; "bytes long."
  1357.  
  1358.  
  1359. '---- Close the file.
  1360. CALL CloseFile(Handle)
  1361. GOSUB HandleErr
  1362. PRINT "The file was successfully closed."
  1363.  
  1364.  
  1365. '---- Open the file again, this time for shared access.
  1366. OpenMethod = 66                 'full sharing, read/write
  1367. CALL OpenFile(FileName$, OpenMethod, Handle)
  1368. GOSUB HandleErr
  1369. PRINT FileName$; " successfully opened in shared mode";
  1370. PRINT ", handle:"; Handle
  1371.  
  1372.  
  1373. '---- Lock bytes 50 through 59.
  1374. Start& = 50
  1375. Length& = 10
  1376. Action = 0                      'specify locking
  1377. CALL LockFile(Handle, Start&, Length&, Action)
  1378. GOSUB HandleErr
  1379. PRINT "File bytes 50 through 59 are successfully locked."
  1380.  
  1381.  
  1382. '---- Prove that it is locked by asking DOS to copy it.
  1383. PRINT "DOS (another process) fails to access the file:"
  1384. SHELL "COPY " + FileName$ + " NUL"
  1385.  
  1386.  
  1387. '---- Unlock the same range of bytes (mandatory).
  1388. Start& = 50
  1389. Length& = 10
  1390. Action = 1                      'specify unlocking
  1391. CALL LockFile(Handle, Start&, Length&, Action)
  1392. GOSUB HandleErr
  1393. PRINT "File bytes 50 through 59 successfully unlocked."
  1394.  
  1395.  
  1396. '---- Prove the unlocking worked by having DOS copy it.
  1397. PRINT "Once unlocked DOS can access the file:";
  1398. SHELL "COPY " + FileName$ + " NUL"
  1399.  
  1400.  
  1401. CloseIt:
  1402. '---- Close the file
  1403. CALL CloseFile(Handle)
  1404. GOSUB HandleErr
  1405. PRINT "The file was successfully closed, ";
  1406.  
  1407.  
  1408. '---- Kill the file to be polite
  1409. CALL KillFile(FileName$)
  1410. GOSUB HandleErr
  1411. PRINT "and then successfully deleted."
  1412.  
  1413. END
  1414.  
  1415. '=======================================
  1416. '  Error handler
  1417. '=======================================
  1418. HandleErr:
  1419.  
  1420. TempErr = DOSError%             'call DOSError% just once
  1421. IF TempErr = 0 THEN RETURN      'return if no errors
  1422. PRINT ErrMessage$(TempErr)      'else print the message
  1423. IF TempErr = 1 THEN             'we failed trying to lock
  1424.   COLOR 7 + 16
  1425.   PRINT "SHARE must be installed to continue."
  1426.   COLOR 7
  1427.   RETURN CloseIt
  1428. ELSE                            'otherwise end
  1429.   END
  1430. END IF
  1431.  
  1432.  
  1433. SUB ClipFile (Handle, Length&) STATIC
  1434.   '-- Use SeekFile to seek there, and then call WriteFile
  1435.   '   specifying zero bytes to truncate it at that point.
  1436.   '   Length& + 1 is needed because we need to seek just
  1437.   '   PAST the point where the file is to be truncated.
  1438.   CALL SeekFile(Handle, Length& + 1, Zero)
  1439.   IF ErrCode THEN EXIT SUB    'exit if an error occurred
  1440.   CALL WriteFile(Handle, Dummy, Dummy, Zero)
  1441. END SUB
  1442.  
  1443.  
  1444. SUB CloseFile (Handle) STATIC
  1445.   ErrCode = 0                   'assume no errors
  1446.   Registers.AX = &H3E00         'close file service
  1447.   Registers.BX = Handle         'using this handle
  1448.   CALL DOSInt(Registers)
  1449.   IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
  1450. END SUB
  1451.  
  1452.  
  1453. FUNCTION DOSError%
  1454.   DOSError% = ErrCode           'simply return the error
  1455. END FUNCTION
  1456.  
  1457.  
  1458. FUNCTION ErrMessage$ (ErrNumber) STATIC
  1459.   SELECT CASE ErrNumber
  1460.     CASE 2
  1461.       ErrMessage$ = "File not found"
  1462.     CASE 3
  1463.       ErrMessage$ = "Path not found"
  1464.     CASE 4
  1465.       ErrMessage$ = "Too many files"
  1466.     CASE 5
  1467.       ErrMessage$ = "Access denied"
  1468.     CASE 6
  1469.       ErrMessage$ = "Invalid handle"
  1470.     CASE 61
  1471.       ErrMessage$ = "Disk full"
  1472.     CASE ELSE
  1473.       ErrMessage$ = "Undefined error: " + STR$(ErrNumber)
  1474.   END SELECT
  1475. END FUNCTION
  1476.  
  1477.  
  1478. SUB FlushFile (Handle) STATIC
  1479.   ErrCode = 0                   'assume no errors
  1480.   Registers.AX = &H4500         'create duplicate handle
  1481.   Registers.BX = Handle         'based on this handle
  1482.  
  1483.   CALL DOSInt(Registers)
  1484.   IF Registers.Flags AND 1 THEN 'an error, assign it
  1485.     ErrCode = Registers.AX
  1486.   ELSE                          'no error, so closing the
  1487.     TempHandle = Registers.AX   'dupe flushes the data
  1488.     CALL CloseFile(TempHandle)
  1489.   END IF
  1490. END SUB
  1491.  
  1492.  
  1493. SUB KillFile (FileName$) STATIC
  1494.   ErrCode = 0                      'assume no errors
  1495.   LocalName$ = FileName$ + CHR$(0) 'make an ASCIIZ string
  1496.  
  1497.   Registers.AX = &H4100            'delete file service
  1498.   Registers.DX = SADD(LocalName$)  'using this handle
  1499.   Registers.DS = SSEG(LocalName$)  'use this with PDS
  1500.  'Registers.DS = -1                'use this with QB
  1501.  
  1502.   CALL DOSInt(Registers)
  1503.   IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
  1504. END SUB
  1505.  
  1506.  
  1507. FUNCTION LocFile& (Handle) STATIC
  1508.   ErrCode = 0               'assume no errors
  1509.  
  1510.   Registers.AX = &H4201     'seek to where we are now
  1511.   Registers.BX = Handle     'using this handle
  1512.   Registers.CX = 0          'move zero bytes from here
  1513.   Registers.DX = 0
  1514.  
  1515.   CALL DOSInt(Registers)
  1516.   IF Registers.Flags AND 1 THEN    'an error occurred
  1517.     ErrCode = Registers.AX
  1518.   ELSE                             'adjust to one-based
  1519.     LocFile& = (Registers.AX + (65536 * Registers.DX)) + 1
  1520.   END IF
  1521. END FUNCTION
  1522.  
  1523.  
  1524. SUB LockFile (Handle, Location&, NumBytes&, Action) STATIC
  1525.   ErrCode = 0                     'assume no errors
  1526.   LocalLoc& = Location& - 1       'adjust to zero-based
  1527.  
  1528.   Registers.AX = Action + (256 * &H5C)  'lock/unlock
  1529.   Registers.BX = Handle
  1530.   Registers.CX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&) + 2)
  1531.   Registers.DX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&))
  1532.   Registers.SI = PeekWord%(VARSEG(NumBytes&), VARPTR(NumBytes&) + 2)
  1533.   Registers.DI = PeekWord%(VARSEG(NumBytes&), VARPTR(NumBytes&))
  1534.  
  1535.   CALL DOSInt(Registers)
  1536.   IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
  1537. END SUB
  1538.  
  1539.  
  1540. FUNCTION LofFile& (Handle)
  1541.   '---- first get and save the current file location
  1542.   CurLoc& = LocFile&(Handle) 'LocFile also clears ErrCode
  1543.   IF ErrCode THEN EXIT FUNCTION
  1544.  
  1545.   Registers.AX = &H4202      'seek to the end of the file
  1546.   Registers.BX = Handle      'using this handle
  1547.   Registers.CX = 0           'move zero bytes from there
  1548.   Registers.DX = 0
  1549.  
  1550.   CALL DOSInt(Registers)
  1551.   IF Registers.Flags AND 1 THEN  'an error occurred
  1552.     ErrCode = Registers.AX
  1553.     EXIT FUNCTION
  1554.   ELSE                           'assign where we are
  1555.     LofFile& = Registers.AX + (65536 * Registers.DX)
  1556.   END IF
  1557.  
  1558.   Registers.AX = &H4200     'seek to where we were before
  1559.   Registers.BX = Handle     'using this handle
  1560.   Registers.CX = PeekWord%(VARSEG(CurLoc&), VARPTR(CurLoc&) + 2)
  1561.   Registers.DX = PeekWord%(VARSEG(CurLoc&), VARPTR(CurLoc&))
  1562.  
  1563.   CALL DOSInt(Registers)
  1564.   IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
  1565. END FUNCTION
  1566.  
  1567.  
  1568. SUB OpenFile (FileName$, Method, Handle) STATIC
  1569.   ErrCode = 0                          'assume no errors
  1570.   Registers.AX = Method + (256 * &H3D) 'open file service
  1571.   LocalName$ = FileName$ + CHR$(0) 'make an ASCIIZ string
  1572.  
  1573.   DO
  1574.     Registers.DX = SADD(LocalName$) 'point to the name
  1575.     Registers.DS = SSEG(LocalName$) 'use this with PDS
  1576.    'Registers.DS = -1               'use this w/QuickBASIC
  1577.  
  1578.     CALL DOSInt(Registers)              'call DOS
  1579.     IF (Registers.Flags AND 1) = 0 THEN 'no errors
  1580.       Handle = Registers.AX         'assign the handle
  1581.       EXIT SUB                      'and we're all done
  1582.     END IF
  1583.  
  1584.     IF Registers.AX = 2 THEN        'File not found error
  1585.       Registers.AX = &H3C00         'so create it!
  1586.     ELSE
  1587.       ErrCode = Registers.AX        'read the code from AX
  1588.       EXIT SUB
  1589.     END IF
  1590.   LOOP
  1591. END SUB
  1592.  
  1593.  
  1594. SUB ReadFile (Handle, Segment, Address, NumBytes) STATIC
  1595.   ErrCode = 0                   'assume no errors
  1596.  
  1597.   Registers.AX = &H3F00         'read from file service
  1598.   Registers.BX = Handle         'using this handle
  1599.   Registers.CX = NumBytes       'and this many bytes
  1600.   Registers.DX = Address        'read to this address
  1601.   Registers.DS = Segment        'and this segment
  1602.  
  1603.   CALL DOSInt(Registers)
  1604.   IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
  1605. END SUB
  1606.  
  1607.  
  1608. SUB SeekFile (Handle, Location&, Method) STATIC
  1609.   ErrCode = 0                      'assume no errors
  1610.   LocalLoc& = Location& - 1        'adjust to zero-based
  1611.  
  1612.   Registers.AX = Method + (256 * &H42)
  1613.   Registers.BX = Handle
  1614.   Registers.CX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&) + 2)
  1615.   Registers.DX = PeekWord%(VARSEG(LocalLoc&), VARPTR(LocalLoc&))
  1616.  
  1617.   CALL DOSInt(Registers)
  1618.   IF Registers.Flags AND 1 THEN ErrCode = Registers.AX
  1619. END SUB
  1620.  
  1621.  
  1622. SUB WriteFile (Handle, Segment, Address, NumBytes) STATIC
  1623.   ErrCode = 0                      'assume no errors
  1624.  
  1625.   Registers.AX = &H4000
  1626.   Registers.BX = Handle
  1627.   Registers.CX = NumBytes
  1628.   Registers.DX = Address
  1629.   Registers.DS = Segment
  1630.  
  1631.   CALL DOSInt(Registers)
  1632.   IF Registers.Flags AND 1 THEN
  1633.     ErrCode = Registers.AX
  1634.   ELSEIF Registers.AX <> Registers.CX THEN
  1635.     ErrCode = 61
  1636.   END IF
  1637. END SUB
  1638.  
  1639. This program begins by dimensioning two variables as SHARED throughout the
  1640. entire module.  By establishing the Registers TYPE variable as SHARED, all
  1641. of the routines can use the same portion of DGROUP memory.  If a separate
  1642. DIM statement were used within each procedure, that many copies of this 20-
  1643. byte variable would reside in memory at once.  The CRLF$ variable does not
  1644. need to be shared, because it is used only by the demonstration portion of
  1645. the program.
  1646.      Before I describe each of these routines and how they are used, it is
  1647. important to explain how DOS uses file handles.  BASIC is unique among
  1648. languages in that it allows you to make up an arbitrary file number that is
  1649. used to access the files.  With most languages and operating systems--and
  1650. DOS is no exception--it is the operating system that assigns a number which
  1651. your program must remember.  Therefore, when you call the OpenFile routine
  1652. to open a file, the Handle parameter is returned to you and you will use
  1653. that number for subsequent file operations.
  1654.      Another important point is how errors are handled by these routines. 
  1655. Since you do not use ON ERROR to trap those situations another method is
  1656. needed.  Each routine clears or sets a global SHARED variable named
  1657. ErrCode, which indicates its success or failure.  After each call to one of
  1658. these routines you will then check this variable, to see if it was
  1659. successful.  For the most efficiency, this program invokes a central error
  1660. checking GOSUB routine that performs the actual testing.  If an error
  1661. occurs this routine prints an appropriate message using the ErrMessage$
  1662. function, and then ends.  The DOSError function is provided to allow access
  1663. to ErrCode from other modules.
  1664.      In practice, it is not strictly necessary to add an explicit test
  1665. after each subroutine call.  For example, if you know the file has been
  1666. opened successfully and you are sure the disk drive has sufficient space,
  1667. then it is probably safe to assume that subsequent file writes will be
  1668. okay.  However, if you do call a routine that causes an error and don't
  1669. check for that error, the next successful call to another routine will
  1670. clear ErrCode and you will have no way to know about the earlier error.
  1671.  
  1672.  
  1673. Opening a File
  1674.  
  1675. The demonstration begins by first assigning a file name and open method,
  1676. and then calling OpenFile to open the file.  The open method lets you
  1677. indicate the file access mode (reading, writing, or both), and also if the
  1678. file will be accessed on a network.  This parameter is bit-coded, and each
  1679. bit has a parallel equivalent in BASIC's ACCESS READ, WRITE, SHARED, LOCK
  1680. READ, and LOCK WRITE options.  Figure 11-4 shows how these bits are
  1681. organized.
  1682.  
  1683.  
  1684.  7   6   5   4   3   2   1   0  <── Bits
  1685. n/a  64  32  16 n/a  4   2   1  <── Numeric Values
  1686. ═══ ═══ ═══ ═══ ═══ ═══ ═══ ═══
  1687.  │   │   │   │   │   │   │   │
  1688.  │   │   │   │   │   └───┴───┴───── Access Mode
  1689.  │   │   │   │   └───────────────── Reserved
  1690.  │   └───┴───┴───────────────────── Sharing Mode
  1691.  └───────────────────────────────── Inheritance
  1692.  
  1693. Figure 11-4: The organization of the bits that establish how a file is to
  1694. be opened.
  1695.  
  1696.  
  1697. As with the file attribute bits shown earlier in Figure 11-3, you also need
  1698. to set bits individually here to fully control the various file permission
  1699. privileges.  The access mode bits are valid with DOS versions 2.0 or later,
  1700. and are equivalent to BASIC's ACCESS arguments.  The sharing mode bits
  1701. require DOS 3.0 or later, and also require SHARE.EXE to be installed.  Note
  1702. that some network software does not explicitly require SHARE, and provides
  1703. the same functionality as part of its normal operation.
  1704.      The three lower bits control the file access, using the following
  1705. binary code: 000 establishes read-only access, 001 allows writing only, and
  1706. 010 allows both reading and writing.  The term access as used here means
  1707. what actions *your* program can perform, and has nothing to do with network
  1708. or file sharing privileges.
  1709.      File sharing privileges are controlled by the three bits in the upper
  1710. nybble (half-byte), and these determine what actions may be performed by
  1711. other programs while your file is open.  Regardless of what sharing (or
  1712. locking) options you choose, your program always has full permission to
  1713. access the file.  The share bits are organized as follows: 000 means
  1714. sharing is disabled, and this is what you must specify if you are not
  1715. running on a network or when DOS 2.x is installed.  A code of 001 denies
  1716. other programs access to either read from or write to the file, 010 allows
  1717. other programs to read but not write, and 011 allows writing but not
  1718. reading.  A code of 100 indicates full sharing, which lets other programs
  1719. read and write, as long as that part of the file is not locked explicitly.
  1720.      Again, these codes are presented as binary values, and it is up to you
  1721. to determine the correct value based on the settings of the individual
  1722. bits.  This is not as hard as it may sound at first, because you simply add
  1723. up the bit values shown in the table.  For example, to open a file for non-
  1724. network read/write access under any version of DOS you use 000 + 010 = 2,
  1725. which is the value used in the first OPEN example.  To open a file for
  1726. reading and writing and also allow other applications to access it fully
  1727. you instead use 100 + 010 = 64 + 2 = 66.  This is shown in the second OPEN
  1728. statement.  Figure 11-5 lists a few of the possible bit combinations, with
  1729. the equivalent BASIC OPEN options.
  1730.  
  1731.  
  1732.       BASIC OPEN Statement             Bits    Value
  1733. =================================    ========  =====
  1734. OPEN FOR BINARY                      00000010     2
  1735. OPEN FOR BINARY ACCESS READ          00000000     0
  1736. OPEN FOR BINARY ACCESS WRITE         00000001     1
  1737. OPEN FOR BINARY ACCESS READ WRITE    00000010     2
  1738. OPEN FOR BINARY ACCESS READ SHARED   01000000    64
  1739. OPEN FOR BINARY LOCK READ            00110010    50
  1740. OPEN FOR BINARY LOCK WRITE           00100010    34
  1741.  
  1742. Figure 11-5: Bit equivalents for some of BASIC's OPEN options.
  1743.  
  1744.  
  1745. Reading and Writing
  1746.  
  1747. Once the file has been opened successfully, the next step is to show how to
  1748. write a string variable in the same way BASIC does when you use PRINT #. 
  1749. The WriteFile and ReadFile routines each expect four arguments: the DOS
  1750. file handle, the segment and address to save from or read into, and the
  1751. number of bytes.  These are the same parameters that DOS expects, and you
  1752. can see by examining the subprograms that they merely pass this information
  1753. on to DOS.
  1754.      Just before the first call to WriteFile, Msg$ is assigned a short test
  1755. string, and a carriage return and line feed are appended to it manually. 
  1756. Remember, when you use BASIC's PRINT # command it is BASIC that adds these
  1757. bytes for you.  When dealing with DOS directly it is up to you to append
  1758. these characters.  Of course, you would omit these to mimic appending a
  1759. semicolon at the end of a BASIC print line:
  1760.  
  1761.      PRINT #1, Msg$;
  1762.  
  1763. SSEG then determines where the string data segment is, and SADD reports its
  1764. address within that segment.  The QuickBASIC version is shown as a comment,
  1765. and it uses VARSEG instead.  The number of bytes is obtained using LEN, and
  1766. DOS accepts any value up to 65535.  It is imperative that you never pass a
  1767. value of zero for the number of bytes, or DOS will truncate the file at the
  1768. current seek location.  I will discuss this in more detail later on, in the
  1769. section entitled *Beyond BASIC's File Handling*.
  1770.      The next example that writes an integer variable to the file is
  1771. similar, except it uses a fixed length of 2.  BASIC will not let you pass
  1772. different types of data to one subprogram or function, which is why these
  1773. read and write routines are designed to accept a segment and address.
  1774.      ReadFile is not called until later in the demonstration; however, it
  1775. is nearly identical to WriteFile.  Because you must tell ReadFile how many
  1776. bytes are to be read, you should establish some type of system.  One good
  1777. one is the method used by Lotus and described in Chapter 6.  For programs
  1778. that do not need such a heavy-handed approach or that write only strings,
  1779. you could use a simpler technique.  For example, each string could be
  1780. preceded by an integer length word, and that word would be read prior to
  1781. reading each string.  The short code fragment that follows shows how this
  1782. might work.
  1783.  
  1784.      Segment = VARSEG(Length)      'Length is what gets read first
  1785.      Address = VARPTR(Length)
  1786.      CALL ReadFile(Handle, Segment, Address, 2)
  1787.  
  1788.      Work$ = SPACE$(Length)        'make a string that long
  1789.      Segment = SSEG(Work$)         'then read Length bytes into the string
  1790.      Address = SADD(Work$)
  1791.      CALL ReadFile(Handle, Segment, Address, Length)
  1792.  
  1793.  
  1794. Setting and Reading the DOS Seek Location
  1795.  
  1796. The LocFile and LofFile functions are similar to their BASIC LOC and LOF
  1797. counterparts, except that LocFile is really equivalent to the SEEK
  1798. function.  Chapter 6 described the difference between the LOC and SEEK
  1799. functions, and came to the inescapable conclusion that LOC is not nearly as
  1800. useful as SEEK in most situations.
  1801.      The SeekFile subprogram, on the other hand, is equivalent to the
  1802. statement form of BASIC's SEEK, and offers an interesting twist as an
  1803. enhancement.  Where BASIC's SEEK statement expects an offset from the
  1804. beginning of the file, DOS provides additional seek methods.  One lets you
  1805. seek relative to where you are now in the file, and the other is relative
  1806. to the end of the file.  Therefore, I have included a SeekMethod parameter
  1807. with my version of SeekFile, letting you enjoy the same flexibility.
  1808.      If SeekMethod is set to zero, DOS behaves the same as BASIC does and
  1809. bases the new seek location from the beginning of the file.  If SeekMethod
  1810. is instead assigned to 1, the new offset into the file will be based on the
  1811. current location.  Note that you may use both positive *and* negative seek
  1812. values, to move forward and backwards respectively.  Finally, using a
  1813. SeekMethod value of 2 tells DOS to consider the new location as being
  1814. relative to the end of the file.
  1815.      For this method you may also use either a positive or negative value,
  1816. to go beyond the end of the file or some offset before the end.  While
  1817. there is nothing inherently wrong with seeking past the end of a file, if
  1818. any data is written at that point DOS will make that the new file length. 
  1819. And as explained in Chapter 6, the portion of the file that lies between
  1820. the previous end of the file and the current end will hold whatever junk
  1821. happened to be in the sectors that were just assigned to extend the length.
  1822.      One slight complication arises if you are dealing with fixed-length
  1823. record data: you must calculate the appropriate file offset manually.  The
  1824. short one-line DEF FN function below shows how to do this.
  1825.  
  1826.   DEF FNSeekLoc&(RecNumber, RecLen) = ((RecNumber - 1) * CLNG(RecLen)) + 1
  1827.  
  1828.  
  1829. Locking a File
  1830.  
  1831. The LockFile subprogram serves the same purpose as BASIC's LOCK and UNLOCK
  1832. statements.  Because the code to lock and unlock a file are identical
  1833. except for a single instruction, it seemed reasonable to combine the two
  1834. services into one routine.  LockFile expects four arguments: a handle, a
  1835. starting offset, the number of bytes, and an action code.  The starting
  1836. offset and number of bytes use long integer values, to accommodate large
  1837. files.
  1838.      Because DOS's Lock and Unlock services require you to specify the
  1839. range of bytes to be locked, additional effort may be needed on your part. 
  1840. For example, if you are manipulating fixed-length records it is up to you
  1841. to translate record numbers and record ranges to an equivalent binary
  1842. offset and number of bytes.  Fortunately, these values are very easy to
  1843. determine using the following formulas:
  1844.  
  1845.      Location& = (RecNumber - 1) * CLNG(RecLength)
  1846.      NumBytes& = RecLength * CLNG(NumRecords)
  1847.  
  1848. Note how CLNG is necessary to prevent BASIC from creating an overflow error
  1849. if the result of the multiplications exceeds 32767.
  1850.      LockFile can also be used with normal BASIC file handling statements,
  1851. if you merely want to avoid an error from attempting to lock a file that is
  1852. already locked by another process.  This requires you to use BASIC's
  1853. FILEATTR function to obtain the equivalent DOS handle, thus:
  1854.  
  1855.      Handle = FILEATTR(FileNumber, 2)
  1856.  
  1857. Here, FileNumber is the BASIC file number that was specified when the file
  1858. was first opened.  For example, if you used this:
  1859.  
  1860.      OPEN FileName$ FOR RANDOM SHARED AS #4 LEN = RecLength
  1861.  
  1862. then the correct value for FileNumber will be 4.
  1863.  
  1864.  
  1865. Beyond BASIC's File Handling
  1866.  
  1867. Aside from SeekFile's ability to use the end of a file or the current seek
  1868. location as a base point, the routines presented so far merely mimic the
  1869. same capabilities BASIC already provides.  Two notable exceptions, however,
  1870. are ClipFile and FlushFile.
  1871.      The ClipFile subprogram lets you set a new length for a file, and that
  1872. length may be either longer or shorter than the current length.  ClipFile
  1873. takes advantage of a little-known DOS feature that sets a new length for a
  1874. file when you tell it to write zero bytes.  This technique was used in the
  1875. DBPACK.BAS program from Chapter 7, and it let that program remove deleted
  1876. records from the end of a dBASE file.
  1877.      ClipFile begins by calling SeekFile to move the DOS file pointer just
  1878. past the new length specified.  If no error occurred it then calls
  1879. WriteFile to write zero bytes at that point, thus establishing the new
  1880. length.  Notice the way the undefined variable Zero is used rather than a
  1881. literal constant 0.  As you already learned in Chapter 2, when a constant
  1882. is passed to a subprogram or function, BASIC creates code to store a copy
  1883. of the constant in DGROUP, and then passes the address of that copy. 
  1884. Although the variable Zero also requires two bytes of DGROUP memory for
  1885. storage, the code to explicitly place the value there is avoided.  Since an
  1886. unassigned variable is always zero this method can be used with confidence.
  1887.      FlushFile also provides an important service that BASIC does not. 
  1888. When data is written to disk using either BASIC or DOS via direct interrupt
  1889. calls, the last portion that was written is not necessarily on the physical
  1890. disk.  DOS buffers all file writes to minimize the number of disk accesses
  1891. needed, thereby improving the speed of those writes.  BASIC performs
  1892. additional buffering as well, which further improves your program's
  1893. performance.  However, this creates a potential problem because a power
  1894. outage or other disaster will cause any data in the file buffer to be lost.
  1895.      FlushFile calls upon another little-known DOS service called Duplicate
  1896. Handle.  When this service is called with the handle of a file that is
  1897. already open, DOS creates a duplicate handle for the same file.  This
  1898. service is not that useful in and of itself, except for one important
  1899. exception:  When the duplicate handle is subsequently closed, DOS also
  1900. writes the original file's contents to disk and updates the directory entry
  1901. to reflect the current length.  This is exactly what FlushFile does to
  1902. flush the file buffer to disk.
  1903.  
  1904.  
  1905. Error Messages
  1906.  
  1907. The ErrMessage$ function is designed to display an appropriate message if
  1908. an error occurs while using these routines.  DOS has fewer error codes than
  1909. BASIC, and it also uses a completely different numbering system.  The
  1910. ErrMessage$ function returns an error message that is equivalent to BASIC's
  1911. where possible, but based on the DOS error return codes.
  1912.  
  1913.  
  1914. Potential Problems
  1915.  
  1916. Although this collection of file handling routines offers many improvements
  1917. over using equivalent BASIC statements, there is one important issue I have
  1918. not addressed here: handling critical errors.  A critical error is caused
  1919. by attempting to access a floppy disk drive with the drive door open, or no
  1920. disk in place.  At the DOS command line critical errors result in the
  1921. infamous "Abort, Retry, Fail" message.
  1922.      Handling critical errors requires pure assembly language, and is a
  1923. fairly complex undertaking.  Therefore, I have purposely omitted that
  1924. functionality from these routines.  However, add-on library products such
  1925. as QuickPak Professional and P.D.Q. from Crescent Software are written in
  1926. assembly language, and include critical error handling.
  1927.      There is another potential problem you must be aware of when using
  1928. these routines.  When you open a file using BASIC's OPEN statement, and
  1929. then restart the program before the file has been closed, BASIC closes the
  1930. file before running your program again.  This is done automatically and
  1931. without your knowing about it.
  1932.      If you call OpenFile to open a file and then restart the program, the
  1933. original file remains open.  This causes no harm by itself--your program
  1934. will simply receive the next available handle when it calls OpenFile.  But
  1935. at some point you will surely exhaust the available handles.  The problem
  1936. is that you will not be able to save your program, because the BASIC editor
  1937. needs a handle when writing your source code to disk.
  1938.      The solution is to press F6 to go to the Immediate window, and then
  1939. type the following line:
  1940.  
  1941.      FOR X% = 5 TO 20: CALL CloseFile(X%): NEXT
  1942.  
  1943. This closes all of the files your program opened, thus freeing them for use
  1944. by the BASIC editor.  It is essential that you never close DOS handles zero
  1945. through four, because they are in use by the PC.  Since DOS uses these
  1946. handles itself to print to the screen and read keyboard input, closing
  1947. those handles will effectively lock up your PC.  [Also, it is okay to close
  1948. handles 5 through 20, even if your program hasn't opened that many.  That
  1949. is, asking DOS to close a file handle that was never opened does no harm.]
  1950.  
  1951.  
  1952. ACCESSING THE MOUSE
  1953. ===================
  1954.  
  1955. All of the DOS and BIOS system services we have looked at so far rely on
  1956. either the Interrupt routine that comes with BASIC, or the simplified
  1957. DOSInt replacement.  In a similar fashion, accessing the mouse driver also
  1958. requires you to call interrupts.  All of the mouse services are invoked
  1959. using Interrupt &H33, and like DOS and the BIOS they require you to load
  1960. the processor's registers to pass information, and then read them again
  1961. afterward to obtain the results.
  1962.      In this section I will present several useful subroutines that show
  1963. how to access the mouse interrupt.  The first portion discusses the various
  1964. utility routines, and shows how they are used.  Following that, I will
  1965. explain how the routines actually work and interface with the mouse driver.
  1966.  
  1967.  
  1968. MOUSE SERVICES
  1969.  
  1970. The important mouse services provided here are those that turn the mouse
  1971. cursor on and off, position it on the screen and control its color, and let
  1972. you determine which buttons are being pressed and where the cursor is
  1973. presently located.  Other routines show how to restrict the range of the
  1974. mouse cursor's travel, and show how to define new, custom cursor shapes.
  1975.      To reduce the size of your programs I have written a short assembly
  1976. language subroutine called MouseInt.  This is similar to the DOSInt routine
  1977. introduced in Chapter 6, except it is intended for use with the mouse
  1978. interrupt &H33.
  1979.  
  1980. ;MOUSEINT.ASM
  1981.  
  1982. .Model Medium, Basic
  1983.  
  1984. MouseRegs Struc
  1985.   RegAX  DW ?
  1986.   RegBX  DW ?
  1987.   RegCX  DW ?
  1988.   RegDX  DW ?
  1989.   Segmnt DW ?
  1990. MouseRegs Ends
  1991.  
  1992. .Code
  1993.  
  1994. MouseInt Proc Uses SI DS ES, MRegs:Word
  1995.   Mov  SI,MRegs          ;get the address of MouseRegs
  1996.   Mov  AX,[SI+RegAX]     ;load each register in turn
  1997.   Mov  BX,[SI+RegBX]
  1998.   Mov  CX,[SI+RegCX]
  1999.   Mov  DX,[SI+RegDX]
  2000.  
  2001.   Mov  SI,[SI+Segmnt]    ;see what the segment is
  2002.   Or   SI,SI             ;is it zero?
  2003.   Jz   @F                ;yes, skip ahead and use default
  2004.  
  2005.   Cmp  SI,-1             ;is it -1?
  2006.   Je   @F                ;yes, skip ahead
  2007.   Mov  DS,SI             ;no, use the segment specified
  2008.  
  2009. @@:
  2010.   Push DS                ;either way, assign ES=DS
  2011.   Pop  ES
  2012.   Int  33h               ;call the mouse driver
  2013.  
  2014.   Push SS                ;regain access to MouseRegs
  2015.   Pop  DS
  2016.  
  2017.   Mov  SI,MRegs          ;access MouseRegs again
  2018.   Mov  [SI+RegAX],AX     ;save each register in turn
  2019.   Mov  [SI+RegBX],BX
  2020.   Mov  [SI+RegCX],CX
  2021.   Mov  [SI+RegDX],DX
  2022.  
  2023.   Ret                    ;return to BASIC
  2024. MouseInt Endp
  2025. End
  2026.  
  2027. Like DOSInt, this routine also uses a TYPE variable to define the various
  2028. CPU registers that are needed by the mouse driver.  However, fewer
  2029. registers are needed simplifying the TYPE structure.  You should define
  2030. this TYPE variable as follows:
  2031.  
  2032.      TYPE MouseType
  2033.        AX      AS INTEGER
  2034.        BX      AS INTEGER
  2035.        CX      AS INTEGER
  2036.        DX      AS INTEGER
  2037.        Segment AS INTEGER
  2038.      END TYPE
  2039.      DIM MouseRegs AS MouseTYPE
  2040.  
  2041. Since the mouse driver uses only these few registers, you can save a few
  2042. bytes of DGROUP memory by using this subset TYPE instead of the full
  2043. Registers TYPE that DOSInt requires.  Notice the last component called
  2044. Segment.  Unlike the Mouse routine that Microsoft sells as an add-on
  2045. library, MouseInt lets you specify a segment for passing far data to the
  2046. mouse interrupt handler.  For most mouse services you can leave the segment
  2047. set to zero or -1.  Either value tells MouseInt to use BASIC's default data
  2048. segment.  But some services that accept the address of incoming data also
  2049. need to know the data's segment.
  2050.      In the Microsoft version you have no choice but to use static data and
  2051. near memory arrays.  Obviously, this precludes being able to use BASIC PDS
  2052. far strings with that interface routine.  You would instead have to create
  2053. a single fixed-length string or TYPE variable, just to force the data to
  2054. reside in near memory.  When calling MouseInt with a value other than zero
  2055. or -1 for the segment, MouseInt loads both DS and ES with that value.
  2056.      As with the collection of DOS file access routines, the following
  2057. subprograms and functions can be added as a module to your program.  Again,
  2058. you should first make a copy of the source file that is included on the
  2059. accompanying floppy disk, and then delete the demonstration portion of the
  2060. program.  This way, you can also run the original demonstration, and trace
  2061. through it to test each of the mouse services.  Of course, be sure to leave
  2062. the commands that dimension the MouseRegs and MousePresent variables as
  2063. being shared, and also the relevant DECLARE and DEFINT statements.
  2064.  
  2065. 'MOUSE.BAS, demonstrates the various mouse services
  2066.  
  2067. DEFINT A-Z
  2068.  
  2069. '---- assembly language functions and subroutines
  2070. DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
  2071. DECLARE SUB MouseInt (MouseRegs AS ANY)
  2072.  
  2073.  
  2074. '---- BASIC functions and subprograms
  2075. DECLARE FUNCTION Bin2Hex% (Binary$)
  2076. DECLARE FUNCTION MouseThere% ()
  2077. DECLARE FUNCTION WaitButton% ()
  2078. DECLARE SUB CursorShape (HotX, HotY, Shape())
  2079. DECLARE SUB HideCursor ()
  2080. DECLARE SUB MouseTrap (ULRow, ULCol, LRRow, LRCol)
  2081. DECLARE SUB MoveCursor (X, Y)
  2082. DECLARE SUB ReadCursor (X, Y, Buttons)
  2083. DECLARE SUB ShowCursor ()
  2084. DECLARE SUB TextCursor (FG, BG)
  2085.  
  2086. DECLARE SUB Prompt (Message$)   'used for this demo only
  2087.  
  2088.  
  2089. TYPE MouseType                  'similar to DOS RegType
  2090.   AX      AS INTEGER
  2091.   BX      AS INTEGER
  2092.   CX      AS INTEGER
  2093.   DX      AS INTEGER
  2094.   Segment AS INTEGER
  2095. END TYPE
  2096.  
  2097. DIM SHARED MouseRegs AS MouseType
  2098. DIM SHARED MousePresent
  2099. REDIM Cursor(1 TO 32)
  2100.  
  2101. IF NOT MouseThere% THEN         'ensure a mouse is present
  2102.   PRINT "No mouse is installed" '  and initialize it if so
  2103.   END
  2104. END IF
  2105. CLS
  2106.  
  2107.  
  2108. DEF SEG = 0                     'see what type of monitor
  2109. IF PEEK(&H463) <> &HB4 THEN     'if it's color
  2110.   ColorMon = -1                 'remember that for later
  2111.   SCREEN 12                     'this requires a VGA
  2112.   LINE (0, 0)-(639, 460), 1, BF 'paint a blue background
  2113. END IF
  2114.  
  2115.  
  2116. DIM Choice$(1 TO 5)             'display some choices
  2117. LOCATE 1, 1                     'for something to point at
  2118. FOR X = 1 TO 5
  2119.   READ Choice$(X)
  2120.   PRINT Choice$(X);
  2121.   LOCATE , X * 12
  2122. NEXT
  2123. DATA "Choice 1", "Choice 2", "Choice 3"
  2124. DATA "Choice 4", "Choice 5"
  2125.  
  2126.  
  2127. IF NOT ColorMon THEN            'if it's not color
  2128.   CALL TextCursor(-2, -2)       'select a text cursor
  2129. END IF
  2130.  
  2131.  
  2132. CALL ShowCursor
  2133. CALL Prompt("Point the cursor at a choice, and press _
  2134.   a button.")
  2135.  
  2136.  
  2137. DO                              'wait for a button press
  2138.   CALL ReadCursor(X, Y, Button)
  2139. LOOP UNTIL Button
  2140. IF Button AND 4 THEN Button = 3 'for three-button mice
  2141.  
  2142. CALL Prompt("You pressed button" + STR$(Button) + _
  2143.   " and the cursor was at location" + STR$(X) + "," + _
  2144.   STR$(Y) + " - press a button.")
  2145.  
  2146. IF ColorMon THEN                'if it is a color monitor
  2147.   RESTORE Arrow                 '  load a custom arrow
  2148.   GOSUB DefineCursor
  2149. END IF
  2150. Dummy = WaitButton%
  2151.  
  2152.  
  2153. IF ColorMon THEN                'the hardware can do it
  2154.   RESTORE CrossHairs            'set a cross-hairs cursor
  2155.   GOSUB DefineCursor
  2156.   CALL Prompt("Now the cursor is a cross-hairs, press _
  2157.     a button.")
  2158.   Dummy% = WaitButton%
  2159. END IF
  2160.  
  2161.  
  2162. IF ColorMon THEN                'now set an hour glass
  2163.   RESTORE HourGlass
  2164.   GOSUB DefineCursor
  2165. END IF
  2166.  
  2167.  
  2168. CALL Prompt("Now notice how the cursor range is _
  2169.   restricted.  Press a button to end.")
  2170. CALL MouseTrap(50, 50, 100, 100)
  2171. Dummy = WaitButton%
  2172.  
  2173. IF ColorMon THEN                'restore to 640 x 350
  2174.   CALL MouseTrap(0, 0, 349, 639)
  2175. ELSE                            'use CGA bounds for mono!
  2176.   CALL MouseTrap(0, 0, 199, 639)
  2177. END IF
  2178.  
  2179.  
  2180. Dummy = MouseThere%             'reset the mouse driver
  2181. CALL HideCursor                 'and turn off the cursor
  2182. SCREEN 0                        'revert to text mode
  2183. END
  2184.  
  2185.  
  2186. DefineCursor:
  2187.  
  2188. FOR X = 1 TO 32                 'read 32 words of data
  2189.   READ Dat$                     'read the data
  2190.   Cursor(X) = Bin2Hex%(Dat$)    'convert to integer
  2191. NEXT
  2192. CALL CursorShape(Zero, Zero, Cursor())
  2193. RETURN
  2194.  
  2195.  
  2196. Arrow:
  2197.  
  2198. NOTES:
  2199. 'The first group of binary data is the screen mask.
  2200. 'The second group of binary data is the cursor mask.
  2201. 'The cursor color is black where both masks are 0.
  2202. 'The cursor color is XORed where both masks are 1.
  2203. 'The color is clear where the screen mask is 1 and the
  2204. '  cursor mask is 0.
  2205. 'The color is white where the screen mask is 0 and the
  2206. '  cursor mask is 1.
  2207. '
  2208. 'Mouse cursor designs by Phil Cramer.
  2209.  
  2210. '--- this is the screen mask
  2211. DATA "1110011111111111"
  2212. DATA "1110001111111111"
  2213. DATA "1110000111111111"
  2214. DATA "1110000011111111"
  2215. DATA "1110000001111111"
  2216. DATA "1110000000111111"
  2217. DATA "1110000000011111"
  2218. DATA "1110000000001111"
  2219. DATA "1110000000000111"
  2220. DATA "1110000000000011"
  2221. DATA "1110000000000001"
  2222. DATA "1110000000011111"
  2223. DATA "1110001000011111"
  2224. DATA "1111111100001111"
  2225. DATA "1111111100001111"
  2226. DATA "1111111110001111"
  2227.  
  2228. '---- this is the cursor mask
  2229. DATA "0001100000000000"
  2230. DATA "0001010000000000"
  2231. DATA "0001001000000000"
  2232. DATA "0001000100000000"
  2233. DATA "0001000010000000"
  2234. DATA "0001000001000000"
  2235. DATA "0001000000100000"
  2236. DATA "0001000000010000"
  2237. DATA "0001000000001000"
  2238. DATA "0001000000000100"
  2239. DATA "0001000000111110"
  2240. DATA "0001001100100000"
  2241. DATA "0001110100100000"
  2242. DATA "0000000010010000"
  2243. DATA "0000000010010000"
  2244. DATA "0000000001110000"
  2245.  
  2246.  
  2247. CrossHairs:
  2248.  
  2249. DATA "1111111101111111"
  2250. DATA "1111111101111111"
  2251. DATA "1111111101111111"
  2252. DATA "1111000000000111"
  2253. DATA "1111011101110111"
  2254. DATA "1111011101110111"
  2255. DATA "1111011111110111"
  2256. DATA "1000000111000000"
  2257. DATA "1111011111110111"
  2258. DATA "1111011101110111"
  2259. DATA "1111011101110111"
  2260. DATA "1111000000000111"
  2261. DATA "1111111101111111"
  2262. DATA "1111111101111111"
  2263. DATA "1111111101111111"
  2264. DATA "1111111111111111"
  2265.  
  2266. DATA "0000000010000000"
  2267. DATA "0000000010000000"
  2268. DATA "0000000010000000"
  2269. DATA "0000111111111000"
  2270. DATA "0000100010001000"
  2271. DATA "0000100010001000"
  2272. DATA "0000100000001000"
  2273. DATA "0111111000111111"
  2274. DATA "0000100000001000"
  2275. DATA "0000100010001000"
  2276. DATA "0000100010001000"
  2277. DATA "0000111111111000"
  2278. DATA "0000000010000000"
  2279. DATA "0000000010000000"
  2280. DATA "0000000010000000"
  2281. DATA "0000000000000000"
  2282.  
  2283.  
  2284. HourGlass:
  2285.  
  2286. DATA "1100000000000111"
  2287. DATA "1100000000000111"
  2288. DATA "1100000000000111"
  2289. DATA "1110000000001111"
  2290. DATA "1110000000001111"
  2291. DATA "1111000000011111"
  2292. DATA "1111100000111111"
  2293. DATA "1111110001111111"
  2294. DATA "1111110001111111"
  2295. DATA "1111100000111111"
  2296. DATA "1111000000011111"
  2297. DATA "1110000000001111"
  2298. DATA "1110000000001111"
  2299. DATA "1100000000000111"
  2300. DATA "1100000000000111"
  2301. DATA "1100000000000111"
  2302.  
  2303. DATA "0000000000000000"
  2304. DATA "0001111111110000"
  2305. DATA "0000000000000000"
  2306. DATA "0000111111100000"
  2307. DATA "0000100110100000"
  2308. DATA "0000010001000000"
  2309. DATA "0000001010000000"
  2310. DATA "0000000100000000"
  2311. DATA "0000000100000000"
  2312. DATA "0000001010000000"
  2313. DATA "0000011111000000"
  2314. DATA "0000110001100000"
  2315. DATA "0000100000100000"
  2316. DATA "0000000000000000"
  2317. DATA "0001111111110000"
  2318. DATA "0000000000000000"
  2319.  
  2320.  
  2321. FUNCTION Bin2Hex% (Binary$) STATIC  'binary to integer
  2322.   Temp& = 0
  2323.   Count = 0
  2324.  
  2325.   FOR X = LEN(Binary$) TO 1 STEP -1
  2326.     IF MID$(Binary$, X, 1) = "1" THEN
  2327.       Temp& = Temp& + 2 ^ Count
  2328.     END IF
  2329.     Count = Count + 1
  2330.   NEXT
  2331.  
  2332.   IF Temp& > 32767 THEN Temp& = Temp& - 65536
  2333.   Bin2Hex% = Temp&
  2334. END FUNCTION
  2335.  
  2336.  
  2337. SUB CursorShape (HotX, HotY, Shape()) STATIC
  2338.   IF NOT MousePresent THEN EXIT SUB
  2339.  
  2340.   MouseRegs.AX = 9
  2341.   MouseRegs.BX = HotX
  2342.   MouseRegs.CX = HotY
  2343.   MouseRegs.DX = VARPTR(Shape(1))
  2344.   MouseRegs.Segment = VARSEG(Shape(1))
  2345.  
  2346.   CALL MouseInt(MouseRegs)
  2347. END SUB
  2348.  
  2349.  
  2350. SUB HideCursor STATIC       'turns off the mouse cursor
  2351.   IF NOT MousePresent THEN EXIT SUB
  2352.  
  2353.   MouseRegs.AX = 2
  2354.   CALL MouseInt(MouseRegs)
  2355. END SUB
  2356.  
  2357.  
  2358. FUNCTION MouseThere% STATIC 'reports if a mouse is present
  2359.   MouseThere% = 0           'assume there is no mouse
  2360.   IF PeekWord%(Zero, (4 * &H33) + 2) = 0 THEN 'segment = 0
  2361.     EXIT FUNCTION           '  means there's no mouse
  2362.   END IF
  2363.  
  2364.   MouseRegs.AX = 0
  2365.   CALL MouseInt(MouseRegs)
  2366.   MouseThere% = MouseRegs.AX
  2367.   IF MouseRegs.AX THEN MousePresent = -1
  2368. END FUNCTION
  2369.  
  2370.  
  2371. SUB MouseTrap (ULRow, ULColumn, LRRow, LRColumn) STATIC
  2372.   IF NOT MousePresent THEN EXIT SUB
  2373.  
  2374.   MouseRegs.AX = 7           'restrict horizontal movement
  2375.   MouseRegs.CX = ULColumn
  2376.   MouseRegs.DX = LRColumn
  2377.   CALL MouseInt(MouseRegs)
  2378.  
  2379.   MouseRegs.AX = 8           'restrict vertical movement
  2380.   MouseRegs.CX = ULRow
  2381.   MouseRegs.DX = LRRow
  2382.   CALL MouseInt(MouseRegs)
  2383. END SUB
  2384.  
  2385.  
  2386. SUB MoveCursor (X, Y) STATIC 'positions the mouse cursor
  2387.   IF NOT MousePresent THEN EXIT SUB
  2388.  
  2389.   MouseRegs.AX = 4
  2390.   MouseRegs.CX = X
  2391.   MouseRegs.DX = Y
  2392.   CALL MouseInt(MouseRegs)
  2393. END SUB
  2394.  
  2395.  
  2396. SUB Prompt (Message$) STATIC 'prints prompt message
  2397.     V = CSRLIN               'save current cursor position
  2398.     H = POS(0)
  2399.     LOCATE 30, 1             'use 25 for EGA SCREEN 9
  2400.     CALL HideCursor          'this is very important!
  2401.     PRINT LEFT$(Message$, 79); TAB(80);
  2402.     CALL ShowCursor          'and so is this
  2403.     LOCATE V, H              'restore the cursor
  2404. END SUB
  2405.  
  2406.  
  2407. SUB ReadCursor (X, Y, Buttons)  'returns cursor and button
  2408.                                 '  information
  2409.   IF NOT MousePresent THEN EXIT SUB
  2410.  
  2411.   MouseRegs.AX = 3
  2412.   CALL MouseInt(MouseRegs)
  2413.  
  2414.   Buttons = MouseRegs.BX AND 7
  2415.   X = MouseRegs.CX
  2416.   Y = MouseRegs.DX
  2417. END SUB
  2418.  
  2419.  
  2420. SUB ShowCursor STATIC        'turns on the mouse cursor
  2421.   IF NOT MousePresent THEN EXIT SUB
  2422.  
  2423.   MouseRegs.AX = 1
  2424.   CALL MouseInt(MouseRegs)
  2425. END SUB
  2426.  
  2427.  
  2428. SUB TextCursor (FG, BG) STATIC
  2429.   IF NOT MousePresent THEN EXIT SUB
  2430.  
  2431.   MouseRegs.AX = 10
  2432.   MouseRegs.BX = 0
  2433.   MouseRegs.CX = &HFF
  2434.   MouseRegs.DX = 0
  2435.  
  2436.   IF FG = -1 THEN        'maintain FG as the cursor moves?
  2437.     MouseRegs.CX = MouseRegs.CX OR &HF00
  2438.   ELSEIF FG = -2 THEN    'invert FG as the cursor moves?
  2439.     MouseRegs.CX = MouseRegs.CX OR &H700
  2440.     MouseRegs.DX = &H700
  2441.   ELSE                   'use the specified color
  2442.     MouseRegs.DX = 256 * (FG AND &HFF)
  2443.   END IF
  2444.  
  2445.   IF BG = -1 THEN        'maintain BG as the cursor moves?
  2446.     MouseRegs.CX = MouseRegs.CX OR &HF000
  2447.   ELSEIF BG = -2 THEN    'invert BG as the cursor moves?
  2448.     MouseRegs.CX = MouseRegs.CX OR &H7000
  2449.     MouseRegs.DX = MouseRegs.DX OR &H7000
  2450.   ELSE                   'use the specified color
  2451.     Temp = (BG AND 7) * 16 * 256
  2452.     MouseRegs.DX = MouseRegs.DX OR Temp
  2453.   END IF
  2454.  
  2455.   CALL MouseInt(MouseRegs)
  2456. END SUB
  2457.  
  2458.  
  2459. FUNCTION WaitButton% STATIC     'waits for a button press
  2460.   IF NOT MousePresent THEN EXIT FUNCTION
  2461.  
  2462.   X! = TIMER                    'pause to allow releasing
  2463.   WHILE X! + .2 > TIMER         '  the button
  2464.   WEND
  2465.  
  2466.   DO                            'wait for a button press
  2467.     CALL ReadCursor(X, Y, Button)
  2468.   LOOP UNTIL Button
  2469.  
  2470.   IF Button AND 4 THEN Button = 3 'for three-button mice
  2471.   WaitButton% = Button            'assign the function
  2472. END FUNCTION
  2473.  
  2474. This program begins by declaring all of the support functions, and then
  2475. defines and dimensions the MouseRegs TYPE variable.  The integer array is
  2476. used to hold the custom graphics cursor shape information, which the
  2477. CursorShape routine requires.  The remainder of the program illustrates how
  2478. to use the various mouse routines in your own programs.
  2479.  
  2480.  
  2481. (2) Determining if a Mouse is Present
  2482.  
  2483.      The first function is MouseThere, which serves two important purposes: 
  2484. The first is to determine if a mouse is present.  The second purpose of
  2485. MouseThere is to initialize the mouse driver to its default parameters. 
  2486. This lets you be sure that the mouse color, shape, and other parameters are
  2487. in a known state.  Resetting the mouse is strongly recommended because some
  2488. programs do not bother to reset the mouse when they are finished.
  2489.      Although there is a mouse service to determine if the driver is
  2490. installed, you must also perform an additional test to prevent problems
  2491. with early computers running DOS version 2.  The problem arises because
  2492. these computers leave the mouse interrupt (&H33) undefined if no mouse is
  2493. present, and calling this interrupt is likely to make the PC crash.
  2494.      As you already know, the interrupt vector table in low memory holds
  2495. the segment and address for every interrupt service routine that is present
  2496. in the PC.  But who puts those addresses into the interrupt vector table? 
  2497. All of the BIOS interrupt addresses are assigned by the BIOS as part of the
  2498. power-up code in your PC's ROM.  Likewise, DOS installs the addresses it
  2499. needs while it is being loaded from disk.
  2500.      The BIOS in modern computers assigns every interrupt vector to a valid
  2501. address, even those that it (the BIOS) does not use.  The code pointed to
  2502. by the unused interrupts is an assembly language Iret (Interrupt Return)
  2503. instruction.  So if no other routine is servicing that interrupt, calling
  2504. it merely returns with no change to the register contents.  But early
  2505. computers and early versions of DOS ignored Interrupt &H33, and left the
  2506. values in that vector address set to zero.  [Calling the "code" at address
  2507. zero is guaranteed to fail, since address zero holds other addresses and
  2508. not executable code.]  Therefore, to safely detect the presence of a mouse
  2509. requires first looking in low memory, to ensure that the interrupt address
  2510. there is valid.
  2511.      It is important to understand that you *must* use MouseThere once at
  2512. the start of your program, before any of the other mouse routines will
  2513. work.  All of the mouse routines check the global variable MousePresent
  2514. before calling MouseInt, and do nothing if it is zero.  This safety
  2515. mechanism lets you freely call the various mouse services without regard to
  2516. whether or not a mouse is installed, to avoid the DOS 2 problem described
  2517. earlier.  Thus, the same program statements can accommodate a mouse if one
  2518. is present or not, without requiring many separate IF tests.
  2519.      For example, you will probably want to write programs that use a mouse
  2520. if one is present, but don't require it.  If you had to have a separate
  2521. block of code for each case, your program would be much larger and slower
  2522. than necessary.  Therefore, you can simply call these mouse routines
  2523. whether or not a mouse is present.  The code fragment that follows shows a
  2524. simple example of this in context.
  2525.  
  2526.      PRINT "Press a key or mouse button to continue: ";
  2527.      DO
  2528.        Temp$ = INKEY$
  2529.        CALL ReadCursor(X, Y, Buttons)
  2530.      LOOP UNTIL LEN(INKEY$) OR Buttons
  2531.      PRINT "Thank you."
  2532.  
  2533. If MouseThere determined that no mouse was present when it was called
  2534. earlier, then ReadCursor will do nothing and return no values.  Of course,
  2535. you will have to check for mouse events and act on them, but these can be
  2536. handled within the same blocks of code that also handle keyboard input.
  2537.      Once the program knows that a mouse is in fact present, it checks to
  2538. see if the display adapter is color or monochrome.  A color monitor
  2539. supports more mouse options such as changing the shape of the mouse cursor. 
  2540. In this case the program assumes that you have a VGA adapter.  If you have
  2541. only an EGA, simply change the SCREEN 12 statement to SCREEN 9.  You will
  2542. also have to change the LOCATE command in the Prompt subprogram to use line
  2543. 25 instead of line 30.  Although the cursor shape can be altered with CGA
  2544. and Hercules adapters, those are not accommodate here.
  2545.      Once the screen display mode is set, a filled box is drawn covering
  2546. the entire screen, to create an attractive blue background.  You should be
  2547. aware that the drivers included come with many older, inexpensive clone
  2548. mouse devices do not support the EGA and VGA display modes.  This is not a
  2549. limitation with the mouse hardware; rather, the problem lies in the driver
  2550. software.  Fortunately, the MOUSE.COM and MOUSE.SYS drivers that Microsoft
  2551. includes with BASIC work with most brands of mouse.  Furthermore, you are
  2552. allowed to distribute those drivers with your own programs, as long as you
  2553. include an appropriate copyright notice.  See the license agreement that
  2554. came with your version of BASIC for more information on displaying the
  2555. Microsoft copyright.
  2556.  
  2557.  
  2558. CONTROLLING THE TEXT CURSOR
  2559.  
  2560. After reading and displaying a list of sample choices that serve as a menu,
  2561. the program again checks to see which type of adapter is present.  If it is
  2562. monochrome, then a custom text cursor is defined using the TextCursor
  2563. routine.  This routine is appropriate for both monochrome and color
  2564. adapters, and offers several useful options that let you control fully how
  2565. the foreground and background colors will appear.  Also, an initial call to
  2566. TextCursor is needed with some non-Microsoft mouse drivers to ensure that
  2567. the cursor is displayed after calling ShowCursor.
  2568.      TextCursor expects two parameters to control the cursor's foreground
  2569. and background colors.  If a positive value is given for either parameter,
  2570. then that is the color the mouse cursor assumes as it travels around the
  2571. screen.  For example, if you use a color combination of 0, 4 the character
  2572. under the mouse cursor will be shown in black on a red background.  It is
  2573. important to understand that the normal mouse cursor color is actually the
  2574. character's background color.  The foreground indicates what color the text
  2575. is to become as the cursor passes over it.
  2576.      Using a value of -1 for either parameter tells the mouse driver to
  2577. leave that portion of the color alone when the cursor is positioned over a
  2578. character.  If you use a color combination of 7, -1 the text under the
  2579. mouse cursor will be shown in white and the background will be unchanged. 
  2580. Of course, if both the foreground and background are set to -1, the cursor
  2581. will never be visible.
  2582.      A value of -2 causes that color portion to be inverted using an XOR
  2583. process as the cursor moves around the screen.  That is, white becomes
  2584. black, green turns to magenta, and blue is translated to brown.  Although a
  2585. value of -2 for the background guarantees that the cursor is always
  2586. visible, it can also be distracting to see the mouse cursor color change
  2587. constantly when the screen itself uses many colors.  If you want to
  2588. experiment with the various TextColor options, add remarking apostrophes to
  2589. deactivate the three statements after the line IF PEEK(&H463) <> &HB4 THEN
  2590. near the beginning of the program.
  2591.      The ShowCursor subprogram simply tells the mouse drive to make the
  2592. mouse cursor visible, in much the same way LOCATE , , 1 option does with
  2593. the normal screen cursor.  The companion routine HideCursor turns the mouse
  2594. cursor off again.  These are very simple routines that do not require much
  2595. explanation; however, please understand that until you turn the cursor on
  2596. explicitly it remains hidden.  As a rule, you also want to ensure that the
  2597. cursor is turned off before you end your program and return to DOS.
  2598.      There is one irritating quirk about how the mouse driver keeps track
  2599. of whether the mouse cursor is currently visible or not.  When you use the
  2600. statement LOCATE , , 0 to turn off the regular text cursor, the BIOS
  2601. remembers that it is off.  And if you subsequently use the same statement
  2602. again the request is ignored.  The mouse driver, on the other hand,
  2603. remembers how many times you called HideCursor and requires a corresponding
  2604. number of calls to ShowCursor before it becomes visible.  However, the
  2605. reverse is not true.  If you turn on the cursor, say, five times in a row,
  2606. only one call to HideCursor is needed to turn it off.
  2607.  
  2608.  
  2609. READING THE MOUSE BUTTONS AND CURSOR POSITION
  2610.  
  2611. The next mouse routine is called ReadCursor, and it calls the service that
  2612. returns both the current mouse cursor position and also which buttons are
  2613. currently pressed.  Notice that the X and Y values returned assume graphics
  2614. pixel coordinates even when the display screen is in text mode!  Therefore,
  2615. when a monochrome display adapter is being used, the values returned range
  2616. from 0 to 639 horizontally (X), and 0 through 199 vertically (Y).  These
  2617. are the same values you would receive when in CGA black and white screen
  2618. mode 2.  When in graphics mode, the X and Y values are based on the current
  2619. SCREEN setting.  For example, in EGA screen mode 9, the returned value for
  2620. X ranges from 0 through 639, and Y is between 0 and 349.
  2621.      When your program is in text mode (SCREEN 0), the current X and Y
  2622. cursor location is based on the upper-left corner of the mouse cursor box. 
  2623. Therefore, the actual horizontal range (X) is usually returned between 0
  2624. and 632 to account for a box width of 8 pixels.  The vertical location (Y)
  2625. ranges from 0 to 192 for the same reason: If the bottom of the cursor is at
  2626. the bottom of the screen, then the top is eight pixels higher.  In graphics
  2627. mode you are allowed to establish any portion of the mouse cursor as being
  2628. the *hot spot*, and this is discussed below in the section "Changing the
  2629. Mouse Cursor Shape".
  2630.      The buttons are returned bit coded--the lowest bit is set if button 1
  2631. is pressed, and the next bit is set when the second button is pressed.  If
  2632. a mouse has three buttons, the third bit may also be set to indicate that. 
  2633. Isolating which bit or combination of bits is set is done using the AND
  2634. logic operator.  If Button AND 1 is non-zero then the first button is
  2635. pressed.  Similarly, Button AND 2 means the second button is being pressed. 
  2636. However, testing for button 3 requires a value of 4, since that is the
  2637. value of the third bit.  The program fragment that follows shows this in
  2638. context, and you can press one or more buttons at a time.
  2639.  
  2640. DO 
  2641.   PRINT "Press Ctrl-Break to end."
  2642.   CALL ReadCursor(X, Y, Button) 
  2643.  
  2644.   LOCATE 10, 1 
  2645.   IF Button AND 1 THEN 
  2646.     PRINT "BUTTON 1" 
  2647.   ELSE 
  2648.     PRINT "        " 
  2649.   END IF 
  2650.  
  2651.   LOCATE 10, 11 
  2652.   IF Button AND 2 THEN 
  2653.     PRINT "BUTTON 2" 
  2654.   ELSE 
  2655.     PRINT "        " 
  2656.   END IF 
  2657.  
  2658.   LOCATE 10, 21 
  2659.   IF Button AND 4 THEN 
  2660.     PRINT "BUTTON 3" 
  2661.   ELSE 
  2662.     PRINT "        " 
  2663.   END IF 
  2664. LOOP 
  2665.  
  2666. Besides the ReadCursor routine which returns the cursor position and button
  2667. status, I have also included a related function called WaitButton.  If your
  2668. program will be waiting for a button and needs to know which button was
  2669. pressed, WaitButton does this using fewer bytes of compiler-generated code. 
  2670. Since there are no passed parameters only five bytes are needed to call
  2671. WaitButton, compared to 17 needed to call ReadCursor.  WaitButton simply
  2672. waits in an empty loop until a button is pressed, and then reports which
  2673. button it was.
  2674.  
  2675.  
  2676. CHANGING THE MOUSE CURSOR SHAPE
  2677.  
  2678. The CursorShape routine lets you change the size and shape of the mouse
  2679. cursor when the display is in graphics mode.  The mouse driver routine that
  2680. is called requires the address of a block of memory 32 words long that
  2681. holds the new shape and color information.  The data in this memory block
  2682. is organized into two sections.  The first 16 words hold what is called the
  2683. *screen mask*, and the second 16 words hold the *cursor mask*.
  2684.      The bits in these masks interact to change the way the foreground and
  2685. background colors on the screen change as the cursor passes over them.  The
  2686. method used by the mouse driver to control the cursor shape and colors is
  2687. very complex, and the examples and discussions in Microsoft's documentation
  2688. do little to assist the programmer.  Therefore, I have provided a simple
  2689. mechanism that lets you draw the cursor shape using a series of BASIC DATA
  2690. statements.
  2691.      Using this method it is easy to control each individual pixel in the
  2692. mouse cursor, and determine if it is white, black, or transparent.  When
  2693. the bits in both the screen and cursor masks are both zero, the cursor will
  2694. be black.  And when the bits in both masks are set to 1, the color is XORed
  2695. (reversed) at that pixel position.  If a screen mask bit is 1 and its
  2696. corresponding bit in the cursor mask is 0, the cursor is transparent. 
  2697. Reversing this to make the screen mask 0 and the cursor mask 1 makes the
  2698. cursor white at that position.  Thus, you can create nearly any shape for
  2699. the mouse cursor, and a wide variety of interesting color effects.
  2700.      If your needs are modest or to minimize the number of DATA statements,
  2701. you can define only the cursor mask and use -1 for the first 16 elements in
  2702. the array by changing that portion of the program like this:
  2703.  
  2704.  
  2705. DefineCursor:
  2706.  
  2707. FOR X = 1 TO 32                'read 32 words of data
  2708.   IF X < 17 THEN               'set first 16 elements = -1
  2709.     Cursor(X) = -1
  2710.   ELSE                         'and for the second 16
  2711.     READ Dat$                  '  read the data and then
  2712.     Cursor(X) = Bin2Hex%(Dat$) '  convert to an integer
  2713.   END IF
  2714. NEXT
  2715.  
  2716. DATA "1100000000000000"        'use only 16 DATA items
  2717. DATA "1110000000000000"        '  in this section
  2718.  .
  2719.  .
  2720.  
  2721.  
  2722. The other two parameters required by CursorShape are the X and Y cursor hot
  2723. spots.  When you call ReadCursor to return the current mouse cursor
  2724. location and button information, the X and Y position returned identifies a
  2725. single pixel on the screen.  Which pixel within the mouse cursor that is
  2726. reported is the cursor hot spot.  When you use an arrow cursor shape, the
  2727. hot spot is typically the tip of the arrow.  This is located in the upper
  2728. left corner of the cursor box and is identified as location 0, 0.  However,
  2729. you can also make any other portion of the cursor the hot spot.  For
  2730. simplicity, the GOSUB routine at the DefineCursor label always uses 0, 0. 
  2731. However, the cross hairs cursor really should use the values 8, 8 to set
  2732. the hot spot at the center of the block.
  2733.  
  2734.  
  2735. CONTROLLING THE MOUSE CURSOR POSITION AND RANGE
  2736.  
  2737. The MoveCursor routine lets you set a new position for the mouse cursor,
  2738. and it too expects pixel values even when the screen is in text mode. 
  2739. Although MoveCursor is not demonstrated in this program, it is included in
  2740. the interest of completeness.
  2741.      The final mouse subprogram included lets you restrict the range of
  2742. mouse cursor travel, and it is called--appropriately enough--MouseTrap. 
  2743. You pass the upper-left and lower-right boundaries to MouseTrap, and it in
  2744. turns passes those values on to the mouse driver.  Internally, the mouse
  2745. driver lets you restrict the range for horizontal and vertical motion
  2746. independently.  But for simplicity this routines requires both sets of
  2747. values at one time.
  2748.      Like the services that ReadCursor and MoveCursor call, these services
  2749. also expect the cursor bounds to be given as pixels even when in text mode. 
  2750. Also, notice that the mouse driver always forces the cursor into the
  2751. restricted region for you.  That is, if the cursor is in the upper-left
  2752. corner and you call MouseTrap forcing it to stay inside the bottom half of
  2753. the screen, it will be moved to the top of that region.
  2754.      Be aware that MouseTrap is also required if you plan to use the 43- or
  2755. 50-line EGA and VGA text modes.  By default, the mouse driver assumes that
  2756. a text screen has only 25 lines, and will not normally let the mouse cursor
  2757. be placed below that line.  If you have used WIDTH , 50 to put the screen
  2758. into the 50-line mode, the mouse cursor will not be allowed below line 25. 
  2759. Therefore, you must use MouseTrap to increase the allowable cursor region
  2760. beyond the default range.  Also be aware that using values larger than the
  2761. current screen dimensions let the mouse disappear off the bottom of the
  2762. screen, or wrap around past the right edge and reappear on the left side.
  2763.  
  2764.  
  2765. ACCESSING THE MOUSE DRIVER
  2766.  
  2767. All of the mouse routines considered so far are comprised of a simplified
  2768. interface to the mouse driver through the MouseInt routine.  MouseInt lets
  2769. you access any service supported by the mouse driver, including those that
  2770. I have not described here.  Similar to the various DOS and BIOS services,
  2771. the mouse driver expects a service number in the AX register.  The other
  2772. registers contain the various expected parameters and returned information,
  2773. and they vary from service to service.
  2774.      There are no errors returned by the mouse driver, so no mechanism is
  2775. needed to handle errors.  For example, if you tell the mouse driver to
  2776. position the cursor off the top edge of the screen, it simply ignores you.
  2777.      Unfortunately, discussing every possible mouse service goes beyond
  2778. what I could ever hope to include in a book about BASIC.  If you want to
  2779. learn more about the services that are available to you, I recommend
  2780. purchasing a good technical reference such as the Microsoft Mouse
  2781. Programmer's Reference.  Other mouse manufacturers also publish their own
  2782. technical manuals, and make them available to the public for a small
  2783. charge.  Thankfully, all of the mouse services are consistent across
  2784. brands, although some brands include more features than defined by
  2785. Microsoft.  Unless you write programs only for your own use, you should
  2786. avoid relying on services that are specific to a single manufacturer.
  2787.  
  2788.  
  2789. ACCESSING EXPANDED MEMORY
  2790. =========================
  2791.  
  2792. The last set of routines I will present show how you can use interrupts to
  2793. access an expanded memory (EMS) driver.  Expanded memory has been available
  2794. for many years, and it provides a way to exceed the normal 640K RAM barrier
  2795. imposed by the 8088 microprocessors.  Newer computers that use an 80286 or
  2796. later processors can use what is called Extended Memory (XMS), and this
  2797. type of memory will eventually become the standard way for all computers in
  2798. the future to access more than 1MB of memory.  Unfortunately, accessing the
  2799. extended memory beyond 1MB on an 80286-based PC is complicated by a design
  2800. deficiency in that CPU chip.  Many people are confused about the difference
  2801. between Expanded and Extended memory, so perhaps a brief explanation is in
  2802. order.
  2803.      Extended memory is a single contiguous block that starts at address
  2804. zero and extends through the highest address available, based on the amount
  2805. of memory that is present in a PC.  Expanded memory, on the other hand, is
  2806. more complex, and uses a technique called *bank switching*.  With bank
  2807. switching, a large amount of memory (up to 16 megabytes) is made available
  2808. to the CPU in 16K blocks.  Each of these blocks is called a page, and only
  2809. four of them can be accessed at one time.  Thus, the term bank switching is
  2810. appropriate because various banks of far memory are switched in and out of
  2811. a near memory address space.
  2812.      The EMS standard requires a 64K contiguous area of near memory within
  2813. the 1MB addressable range to be reserved for use by the EMS driver as a
  2814. *page frame*.  On my own PC the 64k address range from &HE000:0000 through
  2815. &HE000:FFFF is not used for any other purpose, and is therefore available
  2816. for use by an EMS driver.  At any given time, the four 16K blocks of memory
  2817. within this segment can be connected to memory that lies outside of the 1MB
  2818. normal address range.
  2819.      Hardware plug-in EMS boards such as the Intel Above Board contain
  2820. their expanded memory on the board itself.  EMS emulator software instead
  2821. converts the Extended memory on computers so equipped to be accessible
  2822. through the 64K segment within the EMS page frame.  This is achieved
  2823. through hardware switches that allow any area of memory to be remapped to
  2824. any other range of addresses.  In either case, however, Expanded memory is
  2825. made available to an application one page at a time as near memory.
  2826.      Each of the four 16K near memory pages in the EMS page frame are
  2827. called *physical pages*, because they reside in physical memory that can be
  2828. accessed directly by the CPU.  However, many pages of far EMS memory are
  2829. available--up to four at a time--and these are called *logical pages*. 
  2830. This is shown graphically in Figure 11-6.
  2831.  
  2832.  
  2833.                                               │
  2834.                                                            │
  2835.                                               │
  2836.                                                            │
  2837.                                               │
  2838.                                         ──────┼────────────┤
  2839.                                       /       │   Page 73  │
  2840. 1MB boundary -->  ┌────────────┐    /   ──────┼────────────┤
  2841.                   │  ROM BIOS  │  /   /       │   Page 72  │
  2842.                ┌─>╞════════════╡/   /   ──────┼────────────┤
  2843.                │  │   Page 3   │  /   /       │            │
  2844.                │  ├────────────┤/   /         │            │
  2845.       Physical │  │   Page 2   │  /           │            │
  2846.       Pages    │  ├────────────┼──────────────┼────────────┤
  2847.                │  │   Page 1   │              │   Page 45  │
  2848.                │  ├────────────┼──────────────┼────────────┤
  2849.                │  │   Page 0   │   \          │            │
  2850.                └─>├────────────┤\    \────────┼────────────┤
  2851.                   │   DISPLAY  │  \           │   Page 38  │
  2852.                   │   MEMORY   │    \─────────┼────────────┤
  2853. 640K boundary --> ╞════════════╡              │            │
  2854.                   │            │              │            │
  2855.                   │   Normal   │              │     EMS    │
  2856.                   │     DOS    │              │   Logical
  2857.                   │   Memory                       Pages   │
  2858.                                │              │
  2859.                   │                                        │
  2860.                                │              └────────────┘
  2861.                   │
  2862.     Address 0 --> └────────────┘
  2863.  
  2864. Figure 11-6: How EMS logical pages in far memory are mapped onto physical
  2865. pages in conventional memory.
  2866.  
  2867.  
  2868. Here, physical page 0 is connected to logical page 38 in expanded memory,
  2869. physical page 1 to logical page 45, and so forth.  Whenever a program wants
  2870. to access a particular logical page in expanded memory, it calls the EMS
  2871. driver telling it to map that page to one of the four physical pages in the
  2872. page frame segment.  Then, the EMS logical page can be accessed at the near
  2873. memory address within the page frame.
  2874.      For simplicity, all of the routines provided here to handle Expanded
  2875. memory use physical page 0 only.  Since these routines merely copy array
  2876. data back and forth between conventional and Expanded memory, the data can
  2877. be copied in blocks of 16K and there is no need to have to map multiple
  2878. pages simultaneously.  Therefore, these routines always map physical page 0
  2879. to whichever logical page needs to be accessed, and then copy the data in
  2880. that page only.
  2881.  
  2882.  
  2883. EMS SERVICES
  2884.  
  2885. As with the DOS services accessed through Interrupt &H21, the EMS driver
  2886. also uses handles to identify which data you are working with.  When memory
  2887. is allocated using EMS Interrupt &H67, you tell the driver how many 16K
  2888. pages you are requesting, and if there is sufficient memory available it
  2889. returns a handle.  It should come as no surprise to learn that these
  2890. parameters are passed using the CPU registers.  Also like DOS and the BIOS,
  2891. the EMS driver expects a service number in the AH Register.  For example,
  2892. the service that requests memory is specified with AH set to &H43.
  2893.      To minimize the amount of code that is added to your programs, I have
  2894. created a short assembly language subroutine called EMSInt that replaces
  2895. the Interrupt routine included with BASIC.  As with DOSInt and MouseInt,
  2896. this routine lets you pass only the parameters that are actually needed, to
  2897. reduce the amount of compiler-generated code.  EMSInt needs access only to
  2898. the AX, BX, CX, and DX registers, so these are the only components in the
  2899. EMSType TYPE structure shown below.
  2900.  
  2901.      TYPE EMSType
  2902.        AX AS INTEGER
  2903.        BX AS INTEGER
  2904.        CX AS INTEGER
  2905.        DX AS INTEGER
  2906.      END TYPE
  2907.  
  2908. Unlike BASIC's Interrupt routine that has to deal with three parameters and
  2909. code to generate any interrupt number, EMSInt itself is relatively simple:
  2910.  
  2911. ;EMSINT.ASM
  2912.  
  2913. .Model Medium, Basic
  2914.  
  2915. EMSRegs Struc
  2916.   RegAX DW ?
  2917.   RegBX DW ?
  2918.   RegCX DW ?
  2919.   RegDX DW ?
  2920. EMSRegs Ends
  2921.  
  2922. .Code
  2923.  
  2924. EMSInt Proc Uses SI, ERegs:Word
  2925.   Mov  SI,ERegs          ;get the address of EMSRegs
  2926.   Mov  AX,[SI+RegAX]     ;load each register in turn
  2927.   Mov  BX,[SI+RegBX]
  2928.   Mov  CX,[SI+RegCX]
  2929.   Mov  DX,[SI+RegDX]
  2930.  
  2931.   Int  67h               ;call the EMS driver
  2932.  
  2933.   Mov  SI,ERegs          ;access EMSRegs again
  2934.   Mov  [SI+RegAX],AX     ;save each register in turn
  2935.   Mov  [SI+RegBX],BX
  2936.   Mov  [SI+RegCX],CX
  2937.   Mov  [SI+RegDX],DX
  2938.  
  2939.   Ret                    ;return to BASIC
  2940. EMSInt Endp
  2941. End
  2942.  
  2943. If you plan to use the mouse and EMS routines in the same program, you
  2944. could use the MouseRegs variable for both and ignore the Segment portion
  2945. when call EMSInt.
  2946.      The program that follows combines a demonstration portion and a
  2947. collection of subprograms and functions.  Notice that like the various
  2948. mouse services, you *must* query EMSThere to ensure that an EMS driver is
  2949. loaded before any of the other routines can be used.
  2950.  
  2951. 'EMS.BAS, demonstrates the EMS memory services
  2952.  
  2953. DEFINT A-Z
  2954.  
  2955. DECLARE FUNCTION Compare% (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, _
  2956.   BYVAL Adr2, NumBytes)
  2957. DECLARE FUNCTION EMSErrMessage$ (ErrNumber)
  2958. DECLARE FUNCTION EMSError% ()
  2959. DECLARE FUNCTION EMSFree& ()
  2960. DECLARE FUNCTION EMSThere% ()
  2961. DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
  2962.  
  2963. DECLARE SUB EMSInt (EMSRegs AS ANY)
  2964. DECLARE SUB EMSStore (Segment, Address, ElSize, NumEls, Handle)
  2965. DECLARE SUB EMSRetrieve (Segment, Address, ElSize, NumEls, Handle)
  2966. DECLARE SUB MemCopy (BYVAL FromSeg, BYVAL FromAdr, BYVAL ToSeg, _
  2967.   BYVAL ToAdr, NumBytes)
  2968.  
  2969. TYPE EMSType                    'similar to DOS Registers
  2970.   AX    AS INTEGER
  2971.   BX    AS INTEGER
  2972.   CX    AS INTEGER
  2973.   DX    AS INTEGER
  2974. END TYPE
  2975.  
  2976. DIM SHARED EMSRegs AS EMSType
  2977. DIM SHARED ErrCode
  2978. DIM SHARED PageFrame
  2979.  
  2980.  
  2981. CLS
  2982. IF NOT EMSThere% THEN           'ensure EMS is present
  2983.   PRINT "No EMS is installed"
  2984.   END
  2985. END IF
  2986.  
  2987. PRINT "This computer has"; EMSFree&;
  2988. PRINT "kilobytes of EMS available"
  2989.  
  2990. REDIM Array#(1 TO 20000)
  2991. FOR X = 1 TO 20000
  2992.   Array#(X) = X
  2993. NEXT
  2994.  
  2995. CALL EMSStore(VARSEG(Array#(1)), VARPTR(Array#(1)), 8, 20000, Handle)
  2996. IF EMSError% THEN
  2997.   PRINT EMSErrMessage$(EMSError%)
  2998.   END
  2999. END IF
  3000.  
  3001. REDIM Array#(1 TO 20000)
  3002. CALL EMSRetrieve(VARSEG(Array#(1)), VARPTR(Array#(1)), 8, 20000, Handle)
  3003. IF EMSError% THEN
  3004.   PRINT EMSErrMessage$(EMSError%)
  3005.   END
  3006. END IF
  3007.  
  3008. FOR X = 1 TO 20000              'prove it worked
  3009.   IF Array#(X) <> X THEN PRINT ".";
  3010. NEXT
  3011. END
  3012.  
  3013.  
  3014. FUNCTION EMSErrMessage$ (ErrNumber) STATIC
  3015.   SELECT CASE ErrNumber
  3016.     CASE 128
  3017.       EMSErrMessage$ = "Internal error"
  3018.     CASE 129
  3019.       EMSErrMessage$ = "Hardware malfunction"
  3020.     CASE 131
  3021.       EMSErrMessage$ = "Invalid handle"
  3022.     CASE 133
  3023.       EMSErrMessage$ = "No handles available"
  3024.     CASE 135, 136
  3025.       EMSErrMessage$ = "No pages available"
  3026.     CASE ELSE
  3027.       IF PageFrame THEN
  3028.         EMSErrMessage$ = "Undefined error: " + STR$(ErrNumber)
  3029.       ELSE
  3030.         EMSErrMessage$ = "EMS not loaded"
  3031.       END IF
  3032.   END SELECT
  3033. END FUNCTION
  3034.  
  3035.  
  3036. FUNCTION EMSError% STATIC
  3037.   Temp& = ErrCode
  3038.   IF Temp& < 0 THEN Temp& = Temp& + 65536
  3039.   EMSError% = Temp& \ 256
  3040. END FUNCTION
  3041.  
  3042.  
  3043. FUNCTION EMSFree& STATIC
  3044.   EMSFree& = 0              'assume failure
  3045.   IF PageFrame = 0 THEN EXIT FUNCTION
  3046.  
  3047.   EMSRegs.AX = &H4200
  3048.   CALL EMSInt(EMSRegs)
  3049.   ErrCode = EMSRegs.AX      'save possible error from AH
  3050.  
  3051.   IF ErrCode = 0 THEN EMSFree& = EMSRegs.BX * 16
  3052. END FUNCTION
  3053.  
  3054.  
  3055. SUB EMSRetrieve (Segment, Address, ElSize, NumEls, Handle) STATIC
  3056.   IF PageFrame = 0 THEN EXIT SUB
  3057.  
  3058.   LocalSeg& = Segment           'use copies we can change
  3059.   LocalAdr& = Address
  3060.  
  3061.   BytesNeeded& = NumEls * CLNG(ElSize)
  3062.   PagesNeeded = BytesNeeded& \ 16384
  3063.   Remainder = BytesNeeded& MOD 16384
  3064.   IF Remainder THEN PagesNeeded = PagesNeeded + 1
  3065.  
  3066.   NumBytes = 16384              'assume we're copying a 
  3067.                                 '  complete page
  3068.   ThisPage = 0                  'start copying to page 0
  3069.  
  3070.   FOR X = 1 TO PagesNeeded      'copy the data
  3071.     IF X = PagesNeeded THEN     'watch out for last page
  3072.       IF Remainder THEN NumBytes = Remainder
  3073.     END IF
  3074.  
  3075.     IF LocalAdr& > 32767 THEN   'handle segment boundaries
  3076.       LocalAdr& = LocalAdr& - &H8000&
  3077.       LocalSeg& = LocalSeg& + &H800
  3078.       IF LocalSeg& > 32767 THEN 
  3079.         LocalSeg& = LocalSeg& - 65536
  3080.       END IF
  3081.     END IF
  3082.  
  3083.     EMSRegs.AX = &H4400       'map physical page 0 to the
  3084.     EMSRegs.BX = ThisPage     '  current logical page
  3085.     EMSRegs.DX = Handle       '  for the given handle
  3086.     CALL EMSInt(EMSRegs)      'then copy the data there
  3087.     ErrCode = EMSRegs.AX      'save possible error from AH
  3088.     IF ErrCode THEN EXIT SUB
  3089.     CALL MemCopy(PageFrame, Zero, CINT(LocalSeg&), CINT(LocalAdr&), _
  3090.       NumBytes)
  3091.  
  3092.     ThisPage = ThisPage + 1
  3093.     LocalAdr& = LocalAdr& + NumBytes
  3094.   NEXT
  3095.  
  3096.   EMSRegs.AX = &H4500           'release memory service
  3097.   EMSRegs.DX = Handle
  3098.   CALL EMSInt(EMSRegs)
  3099.   ErrCode = EMSRegs.AX          'save possible error
  3100. END SUB
  3101.  
  3102.  
  3103. SUB EMSStore (Segment, Address, ElSize, NumEls, Handle) STATIC
  3104.  
  3105.   IF PageFrame = 0 THEN EXIT SUB
  3106.  
  3107.   LocalSeg& = Segment           'use copies we can change
  3108.   LocalAdr& = Address
  3109.  
  3110.   BytesNeeded& = NumEls * CLNG(ElSize)
  3111.   PagesNeeded = BytesNeeded& \ 16384
  3112.   Remainder = BytesNeeded& MOD 16384
  3113.   IF Remainder THEN PagesNeeded = PagesNeeded + 1
  3114.  
  3115.   EMSRegs.AX = &H4300       'allocate memory service
  3116.   EMSRegs.BX = PagesNeeded
  3117.   CALL EMSInt(EMSRegs)
  3118.  
  3119.   ErrCode = EMSRegs.AX      'save possible error from AH
  3120.   IF ErrCode THEN EXIT SUB
  3121.   Handle = EMSRegs.DX       'save the handle returned
  3122.  
  3123.   NumBytes = 16384          'assume we're copying a 
  3124.                             '  complete page
  3125.   ThisPage = 0              'start copying to page 0
  3126.  
  3127.   FOR X = 1 TO PagesNeeded      'copy the data
  3128.     IF X = PagesNeeded THEN     'watch out for last page
  3129.       IF Remainder THEN NumBytes = Remainder
  3130.     END IF
  3131.  
  3132.     IF LocalAdr& > 32767 THEN   'handle segment boundaries
  3133.       LocalAdr& = LocalAdr& - &H8000&
  3134.       LocalSeg& = LocalSeg& + &H800
  3135.       IF LocalSeg& > 32767 THEN 
  3136.         LocalSeg& = LocalSeg& - 65536
  3137.       END IF
  3138.     END IF
  3139.  
  3140.     EMSRegs.AX = &H4400       'map physical page 0 to the
  3141.     EMSRegs.BX = ThisPage     '  current logical page
  3142.     EMSRegs.DX = Handle       '  for the given handle
  3143.     CALL EMSInt(EMSRegs)      'then copy the data there
  3144.     ErrCode = EMSRegs.AX      'save possible error from AH
  3145.     IF ErrCode THEN EXIT SUB
  3146.     CALL MemCopy(CINT(LocalSeg&), CINT(LocalAdr&), PageFrame, Zero, _
  3147.       NumBytes)
  3148.  
  3149.     ThisPage = ThisPage + 1
  3150.     LocalAdr& = LocalAdr& + NumBytes
  3151.   NEXT
  3152. END SUB
  3153.  
  3154.  
  3155. FUNCTION EMSThere% STATIC
  3156.   EMSThere% = 0                 'assume the worst
  3157.   DIM DevName AS STRING * 8
  3158.   DevName = "EMMXXXX0"          'search for this below
  3159.  
  3160.   '---- Try to find the string "EMMXXXX0" at offset 10 in the EMS handler.
  3161.   '     If it's not there then EMS cannot possibly be installed.
  3162.   Int67Seg = PeekWord%(0, (&H67 * 4) + 2)
  3163.   IF NOT Compare%(Int67Seg, 10, VARSEG(DevName$), VARPTR(DevName$), 8) THEN
  3164.     EXIT FUNCTION
  3165.   END IF
  3166.  
  3167.   EMSRegs.AX = &H4100     'get Page Frame Segment service
  3168.   CALL EMSInt(EMSRegs)
  3169.   ErrCode = EMSRegs.AX    'save possible error from AH
  3170.  
  3171.   IF ErrCode = 0 THEN
  3172.     EMSThere% = -1
  3173.     PageFrame = EMSRegs.BX
  3174.   END IF
  3175. END FUNCTION
  3176.  
  3177. EMS.BAS begins by declaring all of the subprograms and functions that it
  3178. uses, as well as the EMSType structure.  The three shared variables are
  3179. used by the various procedures, and should not be removed when you delete
  3180. the demo portion to create a reusable module.
  3181.  
  3182.  
  3183. DETERMINING IF EMS IS PRESENT
  3184.  
  3185. The first function used is EMSThere, which reports if an EMS driver is
  3186. loaded and operative.  EMSThere begins by assuming that an EMS driver is
  3187. not loaded, and assigns a function output value of 0.  Then it attempts to
  3188. find the device name "EMMXXXX0" in the header portion of the EMS device
  3189. driver.  Like the MouseThere function that checked the interrupt vector
  3190. table for a non-zero segment value, this preliminary check is also needed
  3191. to prevent a system lockup on older computers running DOS version 2.
  3192.      To search for this string EMSThere uses PeekWord to retrieve the
  3193. segment for Interrupt &H67, and then looks at the eight bytes at offset 10
  3194. within that segment.  If the Compare function finds the unique identifying
  3195. string, it knows that the driver is loaded and it is safe to invoke
  3196. Interrupt &H67.  Service &H41 returns either -1 in AX if the driver is
  3197. active, or 0 if it is not.  This service also returns the page frame
  3198. segment the driver is using in near memory, and EMSThere saves this value
  3199. in the shared variable PageFrame for access by the other routines.
  3200.  
  3201.  
  3202. DETERMINING AVAILABLE EMS MEMORY
  3203.  
  3204. The second function, EMSFree, returns the number of 16K EMS pages that are
  3205. available to your program.  The remainder of the demonstration simply
  3206. dimensions a 20,000 element double precision array, and then saves it to
  3207. expanded memory.  Because this array exceeds 64K, you must start BASIC with
  3208. the /ah command line switch.  Otherwise you will receive a "Subscript out
  3209. of range" error message.
  3210.      EMSFree uses function &H42 to ask the EMS driver for the number of
  3211. free pages, and the driver returns the page count in BX.  Although it is
  3212. not shown here, service &H42 also returns the total number of pages in the
  3213. DX register.  Therefore, you could easily create a TotalPages function from
  3214. a copy of EMSFree by changing the line that assigns the function output to
  3215. instead be IF ErrCode = 0 THEN TotalPages& = EMSRegs.DX * 16.
  3216.  
  3217.  
  3218. STORING AND RETRIEVING DATA
  3219.  
  3220. The actual storing and retrieving of data to and from Expanded memory is
  3221. fairly complicated, because of the need to map different logical pages to
  3222. physical page zero.  Although Figure 11-6 shows a single group of logical
  3223. pages, the EMS driver really maintains a separate series of logical pages
  3224. for each active handle.
  3225.      EMSStore and EMSRetrieve store and retrieve data in Expanded memory
  3226. respectively, and both of these subprograms are designed to accommodate
  3227. huge arrays larger than 64k.  Therefore, additional work is needed to
  3228. calculate new segment values as each 16K portion has been processed.
  3229.      As with all of the EMS procedures shown here, EMSStore begins by
  3230. verifying that EMSThere has already been invoked, and that a valid page
  3231. frame segment has been obtained.  The next step is to make long integer
  3232. copies of the incoming segment and address parameters.  Because of the
  3233. segment arithmetic that is performed later in the routine, long integers
  3234. are needed to allow values greater than 32,767 to be compared.  Equally
  3235. important, a routine should never alter incoming parameters unless they
  3236. also return information or such changes are expected.
  3237.      Next, EMSStore determines the total number of bytes of EMS storage
  3238. that are needed, and from that calculates the total number of 16K pages. 
  3239. Because the EMS driver allocates entire pages only, an odd number of bytes
  3240. requires an entire additional page.  BASIC's MOD function is used for this,
  3241. and if the result is non-zero, the TotalPages variable is incremented.
  3242.      Once the number of pages is known, service &H43 is called to allocate
  3243. the Expanded memory.  The remainder of the procedure walks through the
  3244. array data in 16K increments, mapping physical page zero to the next
  3245. logical page in sequence.  Note the code that tests the current address to
  3246. see if it is within 32K of spanning a segment boundary.  In that case, the
  3247. address is dropped by 32K, and the segment is increased by an equivalent
  3248. amount.  Because each new segment starts 16 bytes higher than the previous
  3249. one, 32K \ 16 is added to LocalSeg& rather than a full 32K.
  3250.      After the array is stored in EMS, it is redimensioned in the
  3251. demonstration and then retrieved using the EMSRetrieve subprogram. 
  3252. EMSRetrieve is nearly identical to EMSStore, except it copies from EMS to
  3253. the array, and releases memory when it is finished rather than claim it at
  3254. the beginning.  The final step in the demonstration is to examine the value
  3255. in each element, to prove that the array was restored correctly.
  3256.  
  3257.  
  3258. DETECTING EMS ERRORS
  3259.  
  3260. The EMSError function retrieves the current value of ErrCode, and
  3261. manipulates it into a form useable by your programs.  EMS errors are
  3262. returned in the AH register, which requires dividing by 256 to derive a
  3263. single byte value.  But since EMS error numbers start at 128, the value
  3264. returned in AX appears negative to BASIC programs which treat all integers
  3265. as being signed.  This is why a long integer is used initially and then
  3266. converted to a positive value, before dividing to produce the final result.
  3267.      The EMSErrMessage function can be used to display an appropriate
  3268. message if an error is detected.  The incoming error code is filtered
  3269. through a series of CASE statements, based on the error values defined by
  3270. the EMS specification.
  3271.  
  3272.  
  3273. SUGGESTED ENHANCEMENTS
  3274.  
  3275. The routines presented herein provide a limited set of services for
  3276. accessing Expanded memory.  However, there are several improvements you can
  3277. make, and a few other uses that I have not shown.  If you are using BASIC
  3278. PDS [or VB/DOS], one useful enhancement you can add is to change the
  3279. subprograms and functions to receive their parameters by value using the
  3280. BYVAL option.  In fact, this can also be done with the DOS and mouse
  3281. routines, to minimize the amount of code the BASIC compiler adds to your
  3282. final executable program.
  3283.      Although this demonstration shows storing array data only, you can
  3284. also use these routines to store and retrieve text and graphics screens. 
  3285. This is much quicker than saving them to disk, as was shown in Chapter 6. 
  3286. For example, to save a 25 line by 80 column color text screen in Expanded
  3287. memory you would use the appropriate segment and address like this:
  3288.  
  3289.      CALL EMSStore(&HB800, 0, 1, 4000, Handle)
  3290.      CALL EMSRetrieve(&HB800, 0, 1, 4000, Handle)
  3291.  
  3292. Just as you can cause problems by failing to close DOS handles during the
  3293. development of a program, the same problem can happen with an EMS driver. 
  3294. Unfortunately, it is not as easy to know which handle numbers are still
  3295. open if you have not kept track of them yourself manually.  DOS issues its
  3296. handles using a sensible series of sequential numbers.  This is not
  3297. necessarily the case with EMS handles.  The EMM386.EXE driver provided by
  3298. Microsoft does issue sequential handles, starting with handle 1.  But many
  3299. drivers use other starting values, some work from high numbers backwards,
  3300. and yet others use a handle number sequence that is not in order.
  3301.      Finally, to learn about all of the possible EMS services you need a
  3302. good reference.  Although the primary services are shown here, there are
  3303. several others you may find useful.  For example, service &H46 lets you
  3304. retrieve the EMS version number, and service &H4C lets you see how many
  3305. pages are currently allocated for a given handle.  The EMS driver version
  3306. can be valuable, because newer drivers offer more features which you may
  3307. want to take advantage of.  Ray Duncan's book "Advanced MS-DOS" mentioned
  3308. earlier is one good source, and it lists each EMS service and the possible
  3309. errors that can be returned.
  3310.  
  3311.  
  3312. SUMMARY
  3313. =======
  3314.  
  3315. In this chapter you learned how BASIC--and indeed, all languages--use
  3316. interrupts to communicate with the operating system.  You learned what
  3317. interrupts are and how to access them, and how the CPU registers are used
  3318. to communicate information between your program and the interrupt handler
  3319. being invoked.  You also learned how some of the two-byte registers can be
  3320. treated as two one-byte registers, which requires multiplying and dividing
  3321. to access those portions individually.
  3322.      A number of complete programs were presented showing how to access the
  3323. BIOS, DOS, the mouse driver, and Expanded memory.  In the section on BIOS
  3324. interrupts, examples were given that showed how to simulate pressing the
  3325. PrtSc key, and also how to call the video service that clears or scrolls
  3326. only a portion of the display screen.
  3327.      The DOS examples included a complete set of subroutines to replace
  3328. BASIC's file handling statements.  One advantage gained by bypassing BASIC
  3329. is to read and write large amounts of data at one time.  Another is to
  3330. avoid the need for ON ERROR in certain programming situations.  Although
  3331. calling the DOS services directly can be beneficial in many cases, it also
  3332. requires more work on your part.  However, some services cannot be accessed
  3333. using BASIC alone, such as reading file and directory names, or determining
  3334. a file's attribute.  Where BASIC employs string descriptors to know how
  3335. long a string is, DOS instead uses a CHR$(0) zero byte to mark the end.
  3336.      The mouse and Expanded memory discussions described how those
  3337. interrupt services are accessed, and provided practical advice and warnings
  3338. where appropriate.  Although a large number of interrupt routines were
  3339. described, there is a practical limit to how much information can be
  3340. provided here.  In particular, you will need a separate reference manual
  3341. that describes the details of each interrupt service routine in depth.
  3342.      In the next and final chapter you will learn how to program in
  3343. assembly language, and how to add assembly language routines to programs
  3344. you write using BASIC.  Assembly language is unlike any high-level
  3345. language, and it provides the ultimate means to exploit fully all of the
  3346. resources in a PC.
  3347.